diff --git a/packages/web/public/2024springEvent-graph.png b/packages/web/public/2024springEvent-graph.png new file mode 100644 index 000000000..6541f9dcf Binary files /dev/null and b/packages/web/public/2024springEvent-graph.png differ diff --git a/packages/web/src/Font.css b/packages/web/src/Font.css index 8cea15790..ed30ca512 100644 --- a/packages/web/src/Font.css +++ b/packages/web/src/Font.css @@ -25,3 +25,17 @@ url("./static/fonts/NanumSquare_acEB.ttf") format("truetype"); font-weight: 700; } + +@font-face { + font-family: "Galmuri11"; + src: local("Galmuri11"), + url("./static/fonts/Galmuri11.ttf") format("truetype"); + font-weight: 400; +} + +@font-face { + font-family: "Galmuri11"; + src: local("Galmuri11"), + url("./static/fonts/Galmuri11-Bold.ttf") format("truetype"); + font-weight: 700; +} diff --git a/packages/web/src/atoms/event2024SpringInfo.ts b/packages/web/src/atoms/event2024SpringInfo.ts new file mode 100644 index 000000000..5c263fdbc --- /dev/null +++ b/packages/web/src/atoms/event2024SpringInfo.ts @@ -0,0 +1,22 @@ +import { Quest, QuestId } from "@/types/event2024spring"; + +import { atom } from "recoil"; + +export type Event2024SpringInfoType = Nullable<{ + isAgreeOnTermsOfEvent: boolean; + isEligible: boolean; + completedQuests: QuestId[]; + creditAmount: number; + groupCreditAmount: number; + group: number; + ticket1Amount: number; + ticket2Amount: number; + quests: Quest[]; +}>; + +const event2024SpringInfoAtom = atom({ + key: "event2024SpringInfoAtom", + default: null, +}); + +export default event2024SpringInfoAtom; diff --git a/packages/web/src/components/Event/CreditAmoutContainer/index.tsx b/packages/web/src/components/Event/CreditAmoutContainer/index.tsx new file mode 100644 index 000000000..b22367845 --- /dev/null +++ b/packages/web/src/components/Event/CreditAmoutContainer/index.tsx @@ -0,0 +1,62 @@ +import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; + +import WhiteContainer from "@/components/WhiteContainer"; + +import eventTheme from "@/tools/eventTheme"; +import theme from "@/tools/theme"; + +import CoinIcon from "@/static/events/2024springCoin.gif"; + +type CreditAmountContainerProps = Parameters[0]; + +const CreditAmountContainer = ({ + ...whiteContainerProps +}: CreditAmountContainerProps) => { + const { creditAmount, group, groupCreditAmount } = + useValueRecoilState("event2024SpringInfo") || {}; + + return ( + +
+
+ 새터 {group}반 넙죽코인 +
+ coin +
+ {groupCreditAmount ? ("000" + groupCreditAmount).slice(-4) : "000"} +
+
+
+
+ 내가 획득한 넙죽코인 +
+ coin +
+ {creditAmount ? ("000" + creditAmount).slice(-4) : "000"} +
+
+
+ ); +}; + +export default CreditAmountContainer; diff --git a/packages/web/src/components/Event/EventButton/index.tsx b/packages/web/src/components/Event/EventButton/index.tsx new file mode 100644 index 000000000..77bd5cf1f --- /dev/null +++ b/packages/web/src/components/Event/EventButton/index.tsx @@ -0,0 +1,29 @@ +import { HTMLAttributes } from "react"; + +import eventTheme from "@/tools/eventTheme"; +import theme from "@/tools/theme"; + +type EventButtonProps = { + title: string; +} & HTMLAttributes; + +const EventButton = ({ title, ...divProps }: EventButtonProps) => { + return ( +
+ {title} +
+ ); +}; + +export default EventButton; diff --git a/packages/web/src/components/Event/SuggestJoinEventContainer/index.tsx b/packages/web/src/components/Event/SuggestJoinEventContainer/index.tsx new file mode 100644 index 000000000..23ee8fd43 --- /dev/null +++ b/packages/web/src/components/Event/SuggestJoinEventContainer/index.tsx @@ -0,0 +1,100 @@ +import { memo, useState } from "react"; + +import { useIsLogin, useValueRecoilState } from "@/hooks/useFetchRecoilState"; + +import Button from "@/components/Button"; +import { + ModalEvent2024SpringJoin, + ModalNotification, +} from "@/components/ModalPopup"; +import WhiteContainer from "@/components/WhiteContainer"; + +import eventTheme from "@/tools/eventTheme"; +import theme from "@/tools/theme"; + +const SuggestJoinEventContainer = () => { + const isLogin = useIsLogin(); + const { isAgreeOnTermsOfEvent, completedQuests } = + useValueRecoilState("event2024SpringInfo") || {}; + + const [isOpenJoin, setIsOpenJoin] = useState(false); + const [isOpenNotification, setIsOpenNotification] = useState(false); + + const styleButton = { + padding: "12px", + borderRadius: "12px", + ...eventTheme.font12_bold, + background: eventTheme.blue_title, + }; + const styleTitle = { + ...eventTheme.font16_bold, + background: eventTheme.blue_title, + backgroundClip: "text", + textFillColor: "transparent", + marginBottom: "4px", + }; + const styleDescription = { + ...eventTheme.font10, + color: theme.white, + margin: "12px", + }; + + return ( + <> + {!isLogin ? null : !isAgreeOnTermsOfEvent ? ( + +
🌟 첫 발걸음
+
+ 이벤트 참여 동의 이후 퀘스트 달성이 가능합니다. 많은 혜택과 기회를 + 놓치지 마세요! +
+ +
+ ) : completedQuests && !completedQuests.includes("adPushAgreement") ? ( + +
🌟 Taxi의 소울메이트
+
+ Taxi 서비스를 잊지 않도록 가끔 찾아갈게요! 광고성 푸시 알림 수신 + 동의를 해주시면 방이 많이 모이는 시즌, 주변에 택시앱 사용자가 있을 + 때 알려드릴 수 있어요. +
+ +
+ ) : null} + + + + ); +}; + +export default memo(SuggestJoinEventContainer); diff --git a/packages/web/src/components/Footer/index.tsx b/packages/web/src/components/Footer/index.tsx index dec9e66fa..0a2a38ad5 100644 --- a/packages/web/src/components/Footer/index.tsx +++ b/packages/web/src/components/Footer/index.tsx @@ -6,9 +6,10 @@ import { ModalCredit, ModalPrivacyPolicy } from "@/components/ModalPopup"; import ButtonAboveFooter from "./ButtonAboveFooter"; import { ReactComponent as SparcsLogo } from "@/static/assets/sparcsLogos/SparcsLogoWithText.svg"; +import { ReactComponent as SparcsLogoWhite } from "@/static/events/2024SparcsLogoWithTextWhite.svg"; type FooterProps = { - type?: "only-logo" | "full" | "event-2023fall"; + type?: "only-logo" | "full" | "event-2023fall" | "event-2024spring"; children?: ReactNode; }; @@ -72,11 +73,45 @@ const Footer = ({ type = "full", children }: FooterProps) => { )} -
- - - -
+ {type === "event-2024spring" && ( + <> + + + + + + + + + + +
+ + + +
+ + )} + {type !== "event-2024spring" && ( +
+ + + +
+ )} ); }; diff --git a/packages/web/src/components/ModalPopup/Body/BodyEvent2024AbuseWarning.tsx b/packages/web/src/components/ModalPopup/Body/BodyEvent2024AbuseWarning.tsx new file mode 100644 index 000000000..38a898a3a --- /dev/null +++ b/packages/web/src/components/ModalPopup/Body/BodyEvent2024AbuseWarning.tsx @@ -0,0 +1,130 @@ +import { useState } from "react"; + +import Button from "@/components/Button"; +import DottedLine from "@/components/DottedLine"; + +import theme from "@/tools/theme"; + +import CheckRoundedIcon from "@mui/icons-material/CheckRounded"; + +type BodyChatReportSelectUserProps = { + onChangeIsOpen?: (isOpen: boolean) => void; +}; + +const BodyChatReportSelectUser = ({ + onChangeIsOpen, +}: BodyChatReportSelectUserProps) => { + const styleText = { + ...theme.font12, + color: theme.gray_text, + margin: "0 8px 12px", + }; + const styleTextWarn = { + ...theme.font12, + display: "flex", + alignItem: "center", + marginLeft: "5px", + lineHeight: "16px", + color: theme.red_text, + }; + const styleButtons = { + position: "relative", + display: "flex", + justifyContent: "space-between", + gap: "10px", + } as const; + const styleWarn = { + display: "flex", + margin: "12px 8px", + gap: "6px", + ...theme.cursor(), + }; + const styleCheckBox = { + width: "16px", + height: "16px", + overflow: "hidden", + borderRadius: "50%", + background: theme.purple_light, + boxShadow: theme.shadow_purple_input_inset, + transitionDuration: theme.duration, + display: "flex", + alignItems: "center", + justifyContent: "center", + }; + const styleCheckBoxIcon = { + width: "14px", + height: "14px", + fill: theme.white, + }; + const [isAgree, setIsAgree] = useState(false); + + return ( + <> +
+ 짧은 시간 동안 반복적으로 여러 개의 방을 생성하신 것 같습니다. +
+
+ • 방 생성 후 정산/송금 기능을 사용 + 하실 경우, 실제로 택시 탑승이 이루어졌는지 확인하기 위해 Taxi팀에서{" "} + 영수증 등 증빙 서류의 제출을 요청 + 드릴 수 있습니다. +
+
+ • 실제로 택시에 탑승하지 않으신 경우,{" "} + 획득한 넙죽코인이 모두 회수되고 + 추가적인 이벤트 참여가 제한될 수 + 있습니다. +
+
+ • 자세한 이벤트 약관은 {'"'}마이페이지{">"}새터반 택시대제전 이벤트 참여 + 약관{'"'}에서 확인하실 수 있습니다.{" "} +
+
그래도 방을 생성할까요?
+ +
+
setIsAgree(!isAgree)}> +
+ +
+
+ 이 방은 실제로 택시에 탑승하기 위한 목적의 방입니다. +
+
+
+
+ + +
+ + ); +}; + +export default BodyChatReportSelectUser; diff --git a/packages/web/src/components/ModalPopup/Body/BodyEvent2024SpringShare.tsx b/packages/web/src/components/ModalPopup/Body/BodyEvent2024SpringShare.tsx new file mode 100644 index 000000000..cf9e8a345 --- /dev/null +++ b/packages/web/src/components/ModalPopup/Body/BodyEvent2024SpringShare.tsx @@ -0,0 +1,111 @@ +import { useCallback, useEffect, useState } from "react"; +import QRCode from "react-qr-code"; + +import ButtonShare from "@/components/Button/ButtonShare"; +import DottedLine from "@/components/DottedLine"; +import LinkCopy from "@/components/Link/LinkCopy"; +import LinkKakaotalkShare from "@/components/Link/LinkKakaotalkShare"; + +import theme from "@/tools/theme"; + +import { ReactComponent as KakaoTalkLogo } from "@/static/assets/serviceLogos/KakaoTalkLogo.svg"; +import CheckIcon from "@mui/icons-material/Check"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; + +export type BodyEvent2024SpringShareProps = { + inviteUrl: string; + height?: number; +}; + +const BodyEvent2024SpringShare = ({ + height, + inviteUrl, +}: BodyEvent2024SpringShareProps) => { + const { origin } = window.location; + + const [isCopied, setIsCopied] = useState(false); + const onCopy = useCallback(() => setIsCopied(true), [setIsCopied]); + + useEffect(() => { + if (isCopied) { + const timer = setTimeout(() => setIsCopied(false), 1000); + return () => clearTimeout(timer); + } + }, [isCopied]); + + const styleWrapper = height + ? { + height, + display: "flex", + flexDirection: "column" as any, + } + : {}; + + const styleGuide = { + ...theme.font12, + color: theme.gray_text, + margin: "0 8px 12px", + }; + const styleQRSection = { + marginTop: "12px", + position: "relative" as any, + overflow: "hidden", + textAlign: "center" as any, + }; + const styleButtonSection = { + display: "flex", + justifyContent: "center", + gap: "10px", + margin: "12px 0px 0", + }; + + return ( +
+
+ 이벤트를 여러 사람들에게 공유할 수 있습니다. 이 링크를 통해 다른 + 사용자가 이벤트에 참여하면, 회원님과 새 참여자 모두{" "} + 50 넙죽코인을 획득합니다. +
+ +
+
+ +
+
+
+ + } + background="#FFE812" + /> + + + + ) : ( + + ) + } + background={theme.gray_background} + /> + +
+
+ ); +}; + +export default BodyEvent2024SpringShare; diff --git a/packages/web/src/components/ModalPopup/Body/BodyPrivacyPolicy.tsx b/packages/web/src/components/ModalPopup/Body/BodyPrivacyPolicy.tsx index fa9e74ff7..6c3ea5570 100644 --- a/packages/web/src/components/ModalPopup/Body/BodyPrivacyPolicy.tsx +++ b/packages/web/src/components/ModalPopup/Body/BodyPrivacyPolicy.tsx @@ -28,7 +28,7 @@ const BodyPrivacyPolicy = () => { "Taxi")은(는) 「개인정보 보호법」 제30조에 따라 정보주체의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같이 개인정보 처리방침을 수립·공개합니다. -
○ 이 개인정보처리방침은 2023년 3월 4부터 적용됩니다. +
○ 이 개인정보처리방침은 2024년 2월 17부터 적용됩니다.
제1조(개인정보의 처리 목적)
@@ -44,6 +44,15 @@ const BodyPrivacyPolicy = () => { 회원 가입의사 확인, 회원제 서비스 제공에 따른 본인 식별·인증, 회원자격 유지·관리, 서비스 부정이용 방지 목적으로 개인정보를 처리합니다.
+
+ 2. 이벤트 관리 및 상품 전달 +
+ 이벤트 응모 및 경품 추첨 과정에서 해당 이벤트의 참여자에 한해 추가 + 개인정보 수집이 발생할 수 있습니다. 추가로 개인정보를 수집할 경우에는 + 해당 개인정보 수집 시점에서 이용자에게 '수집하는 개인정보 항목', + '개인정보의 수집 및 이용목적', '개인정보의 보유 및 이용기간'에 대해 + 안내하고 동의를 받겠습니다. +
제2조(개인정보의 처리 및 보유 기간)
① < SPARCS >은(는) 법령에 따른 개인정보 보유·이용기간 또는 @@ -56,13 +65,21 @@ const BodyPrivacyPolicy = () => {
- <홈페이지 회원가입 및 관리>와 관련한 개인정보는 수집.이용에 관한 동의일로부터<1년>까지 위 이용목적을 위하여 보유.이용됩니다. -
- 보유근거 : 회원가입 및 탈퇴 의사의 확인, 회원 식별 +
- 보유근거 : 회원가입 및 탈퇴 의사의 확인, 회원 식별, 서비스 + 부정이용자 연락 +
+ - 2.<이벤트 관리 및 상품 전달> +
+ - <이벤트 관리 및 상품 전달>와 관련한 개인정보는 수집.이용에 관한 + 동의일로부터<1년>까지 위 이용목적을 위하여 보유.이용됩니다. +
- 보유근거 : 이벤트 참여 의사 확인, 참여자 식별, 상품 전달, 이벤트 + 부정 참여자 연락
제3조(처리하는 개인정보의 항목)
① < SPARCS >은(는) 다음의 개인정보 항목을 처리하고 있습니다.
- 1< 홈페이지 회원가입 및 관리 > -
- 필수항목 : 이메일, 이름, 접속 로그, 접속 IP 정보, 학번 +
- 필수항목 : 이메일, 이름, 접속 로그, 접속 IP 정보, 학번, 전화번호
제4조(개인정보의 파기절차 및 파기방법)
@@ -177,8 +194,8 @@ const BodyPrivacyPolicy = () => { 피해구제 등을 위하여 아래와 같이 개인정보 보호책임자를 지정하고 있습니다.
- - 성명 :김건 -
- 연락처 :010-5633-6757, geon6757@kaist.ac.kr + - 성명 : 김민찬 +
- 연락처 : 010-8608-7057, kmc7468@kaist.ac.kr
② 정보주체께서는 < SPARCS >("Taxi") 의 서비스를 이용하시면서 발생한 모든 개인정보 보호 관련 문의, 불만처리, 피해구제 등에 관한 사항을 개인정보 보호책임자 및 담당부서로 문의하실 수 있습니다. @@ -196,8 +213,8 @@ const BodyPrivacyPolicy = () => { 신속하게 처리되도록 노력하겠습니다.
- - 담당자 : 김건 -
- 연락처 : 010-5633-6757, geon6757@kaist.ac.kr + - 담당자 : 김민찬 +
- 연락처 : 010-8608-7057, kmc7468@kaist.ac.kr
제10조(정보주체의 권익침해에 대한 구제방법) @@ -225,7 +242,7 @@ const BodyPrivacyPolicy = () => {
제11조(개인정보 처리방침 변경)
- 이 개인정보처리방침은 2023년 3월 4부터 적용됩니다. + 이 개인정보처리방침은 2024년 2월 17부터 적용됩니다.
); diff --git a/packages/web/src/components/ModalPopup/ModalChatPayment.tsx b/packages/web/src/components/ModalPopup/ModalChatPayment.tsx index fd3a74691..21d48efe0 100644 --- a/packages/web/src/components/ModalPopup/ModalChatPayment.tsx +++ b/packages/web/src/components/ModalPopup/ModalChatPayment.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import useAccountFromChats from "@/hooks/chat/useAccountFromChats"; -import { useEvent2023FallQuestComplete } from "@/hooks/event/useEvent2023FallQuestComplete"; +import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; import { useAxios } from "@/hooks/useTaxiAPI"; @@ -50,8 +50,8 @@ const ModalChatPayment = ({ [userOid, roomInfo] ); const onCopy = useCallback(() => setIsCopied(true), [setIsCopied]); - //#region event2023Fall - const event2023FallQuestComplete = useEvent2023FallQuestComplete(); + //#region event2024Spring + const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); //#endregion useEffect(() => { @@ -69,9 +69,9 @@ const ModalChatPayment = ({ method: "post", data: { roomId: roomInfo._id }, onSuccess: () => { - //#region event2023Fall - event2023FallQuestComplete("payingAndSending"); - event2023FallQuestComplete("paying"); + //#region event2024Spring + event2024SpringQuestComplete("payingAndSending"); + event2024SpringQuestComplete("paying"); //#endregion modalProps.onChangeIsOpen?.(false); onRecall?.(); diff --git a/packages/web/src/components/ModalPopup/ModalChatSaveAccount.tsx b/packages/web/src/components/ModalPopup/ModalChatSaveAccount.tsx index 92ff7da9f..43d714e43 100644 --- a/packages/web/src/components/ModalPopup/ModalChatSaveAccount.tsx +++ b/packages/web/src/components/ModalPopup/ModalChatSaveAccount.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from "react"; -import { useEvent2023FallQuestComplete } from "@/hooks/event/useEvent2023FallQuestComplete"; +import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; import { useFetchRecoilState, useValueRecoilState, @@ -32,8 +32,8 @@ const ModalChatSaveAcount = ({ const { account: accountOrigin } = useValueRecoilState("loginInfo") || {}; const [account, setAccount] = useState(accountDefault || ""); const fetchLoginInfo = useFetchRecoilState("loginInfo"); - //#region event2023Fall - const event2023FallQuestComplete = useEvent2023FallQuestComplete(); + //#region event2024Spring + const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); //#endregion useEffect(() => setAccount(accountDefault || ""), [accountDefault]); @@ -45,14 +45,14 @@ const ModalChatSaveAcount = ({ method: "post", data: { account }, onSuccess: () => { - //#region event2023Fall - event2023FallQuestComplete("accountChanging"); + //#region event2024Spring + event2024SpringQuestComplete("accountChanging"); //#endregion fetchLoginInfo(); }, onError: () => setAlert("계좌번호 저장을 실패하였습니다."), }); - }, [account, event2023FallQuestComplete]); + }, [account, event2024SpringQuestComplete]); const styleTitle = { ...theme.font18, diff --git a/packages/web/src/components/ModalPopup/ModalChatSettlement.tsx b/packages/web/src/components/ModalPopup/ModalChatSettlement.tsx index 66102e9d6..d907bb9f0 100644 --- a/packages/web/src/components/ModalPopup/ModalChatSettlement.tsx +++ b/packages/web/src/components/ModalPopup/ModalChatSettlement.tsx @@ -1,7 +1,7 @@ import { useMemo, useRef, useState } from "react"; import useSendMessage from "@/hooks/chat/useSendMessage"; -import { useEvent2023FallQuestComplete } from "@/hooks/event/useEvent2023FallQuestComplete"; +import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; import { useAxios } from "@/hooks/useTaxiAPI"; @@ -40,7 +40,7 @@ const ModalChatSettlement = ({ const isValidAccount = useMemo(() => regExpTest.account(account), [account]); const isRequesting = useRef(false); const sendMessage = useSendMessage(roomInfo._id, isRequesting); - const event2023FallQuestComplete = useEvent2023FallQuestComplete(); + const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); const onClickOk = () => { if (isRequesting.current || !isValidAccount) return; @@ -57,9 +57,9 @@ const ModalChatSettlement = ({ isRequesting.current = false; if (account !== defaultAccount) openSaveAccountModal?.(account); } - //#region event2023Fall - event2023FallQuestComplete("payingAndSending"); - event2023FallQuestComplete("sending"); + //#region event2024Spring + event2024SpringQuestComplete("payingAndSending"); + event2024SpringQuestComplete("sending"); //#endregion modalProps.onChangeIsOpen?.(false); }, diff --git a/packages/web/src/components/ModalPopup/ModalCredit.tsx b/packages/web/src/components/ModalPopup/ModalCredit.tsx index bd3196074..7c9696a05 100644 --- a/packages/web/src/components/ModalPopup/ModalCredit.tsx +++ b/packages/web/src/components/ModalPopup/ModalCredit.tsx @@ -14,6 +14,7 @@ import { members, members2023FallEvent, members2023SpringEvent, + members2024SpringEvent, } from "@/static/members"; type MemberProps = Members[number]["list"][number]; @@ -128,6 +129,11 @@ const ModalCredit = ({ name: t("page_credit.category_all"), body: , }, + { + key: "2024SpringEvent", + name: t("page_credit.category_2024spring_event"), + body: , + }, { key: "2023FallEvent", name: t("page_credit.category_2023fall_event"), diff --git a/packages/web/src/components/ModalPopup/ModalEvent2024SpringAbuseWarning.tsx b/packages/web/src/components/ModalPopup/ModalEvent2024SpringAbuseWarning.tsx new file mode 100644 index 000000000..56f1112eb --- /dev/null +++ b/packages/web/src/components/ModalPopup/ModalEvent2024SpringAbuseWarning.tsx @@ -0,0 +1,39 @@ +import Modal from "@/components/Modal"; + +import BodyEvent2024AbuseWarning from "./Body/BodyEvent2024AbuseWarning"; + +import theme from "@/tools/theme"; + +import ReportGmailerrorredRoundedIcon from "@mui/icons-material/ReportGmailerrorredRounded"; + +type ModalEvent2024AbuseWarningProps = Omit< + Parameters[0], + "padding" | "children" | "onEnter" +>; + +const ModalChatReport = ({ + ...modalProps +}: ModalEvent2024AbuseWarningProps) => { + const styleTitle = { + ...theme.font18, + display: "flex", + alignItems: "center", + margin: "0 8px 12px", + }; + const styleIcon = { + fontSize: "21px", + margin: "0 4px 0 0", + }; + + return ( + +
+ + 경고 +
+ +
+ ); +}; + +export default ModalChatReport; diff --git a/packages/web/src/components/ModalPopup/ModalEvent2024SpringJoin.tsx b/packages/web/src/components/ModalPopup/ModalEvent2024SpringJoin.tsx new file mode 100644 index 000000000..e7771dd13 --- /dev/null +++ b/packages/web/src/components/ModalPopup/ModalEvent2024SpringJoin.tsx @@ -0,0 +1,317 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; + +import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; +import { + useFetchRecoilState, + useIsLogin, + useValueRecoilState, +} from "@/hooks/useFetchRecoilState"; +import { useAxios } from "@/hooks/useTaxiAPI"; + +import Button from "@/components/Button"; +import DottedLine from "@/components/DottedLine"; +import Input from "@/components/Input"; +import Modal from "@/components/Modal"; + +import LinkLogin from "../Link/LinkLogin"; +import ProfileImage from "../User/ProfileImage"; + +import alertAtom from "@/atoms/alert"; +import { useSetRecoilState } from "recoil"; + +import regExpTest from "@/tools/regExpTest"; +import theme from "@/tools/theme"; + +import FestivalRoundedIcon from "@mui/icons-material/FestivalRounded"; + +type ModalEvent2024SpringJoinProps = Parameters[0] & { + inviterId?: string; +}; + +const ModalEvent2024SpringJoin = ({ + inviterId, + ...modalProps +}: ModalEvent2024SpringJoinProps) => { + const axios = useAxios(); + const setAlert = useSetRecoilState(alertAtom); + const isLogin = useIsLogin(); + const { phoneNumber: phoneNumberFromLoginInfo } = + useValueRecoilState("loginInfo") || {}; + const { + isAgreeOnTermsOfEvent, + isEligible, + group: groupFromLoginInfo, + } = useValueRecoilState("event2024SpringInfo") || {}; + const fetchLoginInfo = useFetchRecoilState("loginInfo"); + //#region event2024Spring + const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); + //#endregion + + const [phoneNumber, setPhoneNumber] = useState(""); + const [group, setGroup] = useState(0); + + const [inviterInfo, setInvitorInfo] = useState<{ + profileImageUrl: string; + nickname: string; + }>(); + + const getInvitorInfo = useCallback( + () => + axios({ + url: `/events/2024spring/invite/search/${inviterId}`, + method: "get", + onSuccess: (data) => { + setInvitorInfo(data); + }, + onError: () => setAlert("올바르지 않은 추천인입니다."), + }), + [inviterId] + ); + + const isInvited = !!inviterId; + + useEffect(() => { + if (!isAgreeOnTermsOfEvent && isInvited) getInvitorInfo(); + }, [inviterId]); + + const isValidPhoneNumber = useMemo( + () => regExpTest.phoneNumber(phoneNumber), + [phoneNumber] + ); + const isValidGroup = useMemo(() => group > 0 && group < 27, [group]); + + const onClickJoin = useCallback( + () => + axios({ + url: "/events/2024spring/globalState/create", + method: "post", + data: { phoneNumber, group, inviter: inviterId }, + onSuccess: () => { + fetchLoginInfo(); + //#region event2024Spring + event2024SpringQuestComplete("firstLogin"); + //#endregion + modalProps.onChangeIsOpen?.(false); + }, + onError: () => setAlert("이벤트 참여에 실패하였습니다."), + }), + [phoneNumber, setPhoneNumber, group, setGroup, event2024SpringQuestComplete] + ); + + const styleTitle = { + ...theme.font18, + display: "flex", + alignItems: "center", + margin: "0 8px 12px", + }; + const styleIcon = { + fontSize: "21px", + margin: "0 4px 0 0", + }; + const styleText = { + ...theme.font12, + color: theme.gray_text, + margin: "0 8px", + }; + const styleInputWrap = { + margin: "0 8px 12px", + display: "flex", + alignItems: "center", + color: theme.gray_text, + whiteSpace: "nowrap", + ...theme.font14, + } as const; + + return ( + +
+ + 새터반 택시대제전 이벤트 +
+
+ • 택시 동승을 하지 않는 사용자는{" "} + + 택시 출발 시각이 지나기 전에 탑승 취소 + + 를 하여 방에서 나가야 합니다. +
+
+
+ • 실제 Taxi 동승을 하지 않고{" "} + 허위로 방을 개설하거나 참여하여 + 이벤트 퀘스트를 달성하는 것은{" "} + 부정 이용에 해당됩니다. Taxi 서비스 + 이용 중 서비스를 부정 이용하였다고 판단되거나, 신고를 받은 사용자에게는 + 사안에 따라{" "} + + 이벤트 상품이 지급되지 않을 수 있습니다. + {" "} + 위 경우, SPARCS Taxi팀 서비스 관리자는 서비스 부정 이용을 방지하기 위해 + 택시 탑승을 인증할 수 있는{" "} + 영수증 또는 카카오T 이용기록을 + 요청할 수 있습니다. 또한, 본 서비스를 부정 이용하는 사용자에게는 택시 + 서비스 이용 제한 및 법적 조치를 취할 수 있습니다. +
+
+
+ •{" "} + + 입력해주신 새터반으로 점수가 합산됩니다. + {" "} + 또한, 입력해주신 연락처는 서비스 신고 대응 및 본인 확인을 위해 사용될 수 + 있습니다. +
+
+
+ •{" "} + + 입력해주신 연락처와 새터반은 이후 수정이 불가능합니다. + {" "} +
+
+
+ •{" "} + + 추천인 이벤트 참여를 위해서는 추천인이 발송한 링크로 이벤트에 참여해야 + 합니다. + {" "} + 추천인을 통해 이벤트에 참여할 시, 참가자와 추천인 모두에게 50 넙죽코인이 + 지급됩니다. +
+
+
+ • 본 약관은 동의 이후에도 {'"'}마이페이지{">"}새터반 택시대제전 이벤트 + 참여 약관{'"'}에서 다시 확인하실 수 있습니다.{" "} +
+ {isAgreeOnTermsOfEvent ? ( + <> +
+ +
+
+ 전화번호 + +
+
+ 새터반 + +
+ + + ) : ( + <> + {((isLogin && isEligible) || (isInvited && inviterInfo)) && ( + <> +
+ + + )} +
+ {isLogin && isEligible && ( + <> +
+ 전화번호 + +
+
+ 새터반 + { + const number = parseInt(value, 10); + setGroup(number); + }} + placeholder="숫자만 입력하세요" + css={{ width: "100%", marginLeft: "10px" }} + /> +
+ + )} + {isInvited && inviterInfo && ( +
+ 추천인 +
+ +
+ + {inviterInfo?.nickname} + +
+ )} + {isLogin ? ( + + ) : ( + + + + )} + + )} + + ); +}; + +export default ModalEvent2024SpringJoin; diff --git a/packages/web/src/components/ModalPopup/ModalEvent2024SpringShare.tsx b/packages/web/src/components/ModalPopup/ModalEvent2024SpringShare.tsx new file mode 100644 index 000000000..8c0f8f185 --- /dev/null +++ b/packages/web/src/components/ModalPopup/ModalEvent2024SpringShare.tsx @@ -0,0 +1,49 @@ +import Modal from "@/components/Modal"; + +import BodyEvent2024SpringShare, { + BodyEvent2024SpringShareProps, +} from "./Body/BodyEvent2024SpringShare"; + +import theme from "@/tools/theme"; + +import ShareRoundedIcon from "@mui/icons-material/ShareRounded"; + +type ModalEvent2024SpringShareProps = { + isOpen: boolean; + onChangeIsOpen?: (isOpen: boolean) => void; + inviteUrl: BodyEvent2024SpringShareProps["inviteUrl"]; +}; + +const ModalEvent2024SpringShare = ({ + isOpen, + onChangeIsOpen, + inviteUrl, +}: ModalEvent2024SpringShareProps) => { + const styleTitle = { + ...theme.font18, + display: "flex", + alignItems: "center", + margin: "0 8px 12px", + }; + const styleIcon = { + fontSize: "21px", + margin: "0 4px 0 0", + }; + + return ( + +
+ + 이벤트 공유하기 +
+ +
+ ); +}; + +export default ModalEvent2024SpringShare; +export { BodyEvent2024SpringShare }; diff --git a/packages/web/src/components/ModalPopup/ModalMypageModify.tsx b/packages/web/src/components/ModalPopup/ModalMypageModify.tsx index c8ce20738..dcd267021 100644 --- a/packages/web/src/components/ModalPopup/ModalMypageModify.tsx +++ b/packages/web/src/components/ModalPopup/ModalMypageModify.tsx @@ -2,7 +2,7 @@ import axiosOri from "axios"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useEvent2023FallQuestComplete } from "@/hooks/event/useEvent2023FallQuestComplete"; +import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; import { useFetchRecoilState, useValueRecoilState, @@ -144,8 +144,8 @@ const ModalMypageModify = ({ ...modalProps }: ModalMypageModifyProps) => { const loginInfo = useValueRecoilState("loginInfo"); const fetchLoginInfo = useFetchRecoilState("loginInfo"); - //#region event2023Fall - const event2023FallQuestComplete = useEvent2023FallQuestComplete(); + //#region event2024Spring + const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); //#endregion const setAlert = useSetRecoilState(alertAtom); @@ -169,8 +169,8 @@ const ModalMypageModify = ({ ...modalProps }: ModalMypageModifyProps) => { method: "post", data: { nickname }, onError: () => setAlert(t("page_modify.nickname_failed")), - //#region event2023Fall - onSuccess: () => event2023FallQuestComplete("nicknameChanging"), // event2023Fall + //#region event2024Spring + onSuccess: () => event2024SpringQuestComplete("nicknameChanging"), // event2024Spring //#endregion }); } @@ -181,8 +181,8 @@ const ModalMypageModify = ({ ...modalProps }: ModalMypageModifyProps) => { method: "post", data: { account }, onError: () => setAlert(t("page_modify.account_failed")), - //#region event2023Fall - onSuccess: () => event2023FallQuestComplete("accountChanging"), // event2023Fall + //#region event2024Spring + onSuccess: () => event2024SpringQuestComplete("accountChanging"), // event2024Spring //#endregion }); } diff --git a/packages/web/src/components/ModalPopup/ModalNotification.tsx b/packages/web/src/components/ModalPopup/ModalNotification.tsx index b5139d8f7..f0f7f871a 100644 --- a/packages/web/src/components/ModalPopup/ModalNotification.tsx +++ b/packages/web/src/components/ModalPopup/ModalNotification.tsx @@ -1,7 +1,7 @@ import { useCallback, useRef } from "react"; import { useTranslation } from "react-i18next"; -import { useEvent2023FallQuestComplete } from "@/hooks/event/useEvent2023FallQuestComplete"; +import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; import { useFetchRecoilState, useValueRecoilState, @@ -65,8 +65,8 @@ const ModalNotification = ({ const notificationOptions = useValueRecoilState("notificationOptions"); const fetchNotificationOptions = useFetchRecoilState("notificationOptions"); const isAxiosCalled = useRef(false); - //#region event2023Fall - const event2023FallQuestComplete = useEvent2023FallQuestComplete(); + //#region event2024Spring + const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); //#endregion const styleTitle = { @@ -113,12 +113,12 @@ const ModalNotification = ({ fetchNotificationOptions(); isAxiosCalled.current = false; - //#region event2023Fall + //#region event2024Spring if (optionName === "advertisement" && value) - event2023FallQuestComplete("adPushAgreement"); + event2024SpringQuestComplete("adPushAgreement"); //#endregion }, - [deviceToken, event2023FallQuestComplete] + [deviceToken, event2024SpringQuestComplete] ); const onChangeNotificationAll = useCallback( async (value: boolean) => { @@ -149,11 +149,11 @@ const ModalNotification = ({ fetchNotificationOptions(); isAxiosCalled.current = false; - //#region event2023Fall - if (value) event2023FallQuestComplete("adPushAgreement"); + //#region event2024Spring + if (value) event2024SpringQuestComplete("adPushAgreement"); //#endregion }, - [deviceToken, event2023FallQuestComplete] + [deviceToken, event2024SpringQuestComplete] ); const onChangeNotificationChatting = useCallback( onChangeNotificationOption("chatting"), diff --git a/packages/web/src/components/ModalPopup/ModalRoomShare.tsx b/packages/web/src/components/ModalPopup/ModalRoomShare.tsx index d6d31eeec..daee028b2 100644 --- a/packages/web/src/components/ModalPopup/ModalRoomShare.tsx +++ b/packages/web/src/components/ModalPopup/ModalRoomShare.tsx @@ -1,4 +1,4 @@ -import { useEvent2023FallQuestComplete } from "@/hooks/event/useEvent2023FallQuestComplete"; +import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; import Modal from "@/components/Modal"; @@ -19,8 +19,8 @@ const ModalRoomShare = ({ onChangeIsOpen, roomInfo, }: ModalRoomShareProps) => { - //#region event2023Fall - const event2023FallQuestComplete = useEvent2023FallQuestComplete(); + //#region event2024Spring + const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); //#endregion const styleTitle = { ...theme.font18, @@ -32,10 +32,10 @@ const ModalRoomShare = ({ fontSize: "21px", margin: "0 4px 0 0", }; - //#region Event2023Fall + //#region event2024Spring const onChangeIsOpenWithEvent2023Fall = (isOpen: boolean) => { onChangeIsOpen?.(isOpen); - !isOpen && event2023FallQuestComplete("roomSharing"); + !isOpen && event2024SpringQuestComplete("roomSharing"); }; //#endregion diff --git a/packages/web/src/components/ModalPopup/index.tsx b/packages/web/src/components/ModalPopup/index.tsx index bce1b1830..7ef5423f1 100644 --- a/packages/web/src/components/ModalPopup/index.tsx +++ b/packages/web/src/components/ModalPopup/index.tsx @@ -9,6 +9,9 @@ export { default as ModalEvent2023FallItem } from "./ModalEvent2023FallItem"; export { default as ModalEvent2023FallItemInstagram } from "./ModalEvent2023FallItemInstagram"; export { default as ModalEvent2023FallJoin } from "./ModalEvent2023FallJoin"; export { default as ModalEvent2023FallRandomBox } from "./ModalEvent2023FallRandomBox"; +export { default as ModalEvent2024SpringAbuseWarning } from "./ModalEvent2024SpringAbuseWarning"; +export { default as ModalEvent2024SpringJoin } from "./ModalEvent2024SpringJoin"; +export { default as ModalEvent2024SpringShare } from "./ModalEvent2024SpringShare"; export { default as ModalMypageModify } from "./ModalMypageModify"; export { default as ModalNotification } from "./ModalNotification"; diff --git a/packages/web/src/components/Skeleton/Routes.tsx b/packages/web/src/components/Skeleton/Routes.tsx index 276b1ce66..e72fe0b31 100644 --- a/packages/web/src/components/Skeleton/Routes.tsx +++ b/packages/web/src/components/Skeleton/Routes.tsx @@ -23,7 +23,12 @@ const routeProps = [ exact: true, }, { - path: ["/home", "/home/:roomId"], + path: ["/home", "/home/:roomId", "/home/startEvent/:inviterId"], + component: lazy(() => import("@/pages/Home")), + exact: true, + }, + { + path: "/event/2024spring-invite/:eventStatusId", component: lazy(() => import("@/pages/Home")), exact: true, }, diff --git a/packages/web/src/components/Skeleton/index.tsx b/packages/web/src/components/Skeleton/index.tsx index 5cac93940..46a2bb9db 100644 --- a/packages/web/src/components/Skeleton/index.tsx +++ b/packages/web/src/components/Skeleton/index.tsx @@ -1,7 +1,7 @@ import { ReactNode, useMemo } from "react"; import { useLocation } from "react-router-dom"; -import { useEventEffect } from "@/hooks/event/useEventEffect"; +import { useEvent2024SpringEffect } from "@/hooks/event/useEvent2024SpringEffect"; import useCSSVariablesEffect from "@/hooks/skeleton/useCSSVariablesEffect"; import useChannelTalkEffect from "@/hooks/skeleton/useChannelTalkEffect"; import useFirebaseMessagingEffect from "@/hooks/skeleton/useFirebaseMessagingEffect"; @@ -65,7 +65,7 @@ const Skeleton = ({ children }: SkeletonProps) => { ); //#region event2023Fall - useEventEffect(); + useEvent2024SpringEffect(); //#endregion useSyncRecoilStateEffect(); // loginIngo, taxiLocations, myRooms, notificationOptions 초기화 및 동기화 useI18nextEffect(); @@ -92,7 +92,13 @@ const Skeleton = ({ children }: SkeletonProps) => { )} {children} - {isDisplayNavigation &&
} + {isDisplayNavigation && ( +
+ )} )} diff --git a/packages/web/src/hooks/event/useEventEffect.ts b/packages/web/src/hooks/event/useEvent2023FallEffect.ts similarity index 97% rename from packages/web/src/hooks/event/useEventEffect.ts rename to packages/web/src/hooks/event/useEvent2023FallEffect.ts index e4b0a3a8a..abae6e49a 100644 --- a/packages/web/src/hooks/event/useEventEffect.ts +++ b/packages/web/src/hooks/event/useEvent2023FallEffect.ts @@ -6,7 +6,7 @@ import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; import toast from "@/tools/toast"; -export const useEventEffect = () => { +export const useEvent2023FallEffect = () => { const { completedQuests, quests } = useValueRecoilState("event2023FallInfo") || {}; diff --git a/packages/web/src/hooks/event/useEvent2024SpringEffect.ts b/packages/web/src/hooks/event/useEvent2024SpringEffect.ts new file mode 100644 index 000000000..45f88298e --- /dev/null +++ b/packages/web/src/hooks/event/useEvent2024SpringEffect.ts @@ -0,0 +1,41 @@ +import { useEffect, useRef } from "react"; + +import type { QuestId } from "@/types/event2024spring"; + +import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; + +import toast from "@/tools/toast"; + +export const useEvent2024SpringEffect = () => { + const { completedQuests, quests } = + useValueRecoilState("event2024SpringInfo") || {}; + + const prevEventStatusRef = useRef(); + + useEffect(() => { + if (!completedQuests || !quests) return; + prevEventStatusRef.current = prevEventStatusRef.current || completedQuests; + if (prevEventStatusRef.current.length === completedQuests.length) return; + + const questsForCompare = [...completedQuests]; + prevEventStatusRef.current.forEach((questId) => { + const index = questsForCompare.indexOf(questId); + if (index < 0) return; + questsForCompare.splice(index, 1); + }); + questsForCompare.forEach((questId) => { + const quest = quests.find(({ id }) => id === questId); + if (!quest) return; + const notificationValue = { + type: "default" as const, + imageUrl: quest.imageUrl, + title: "퀘스트 완료", + subtitle: "새터반 택시대제전", + content: `축하합니다! "${quest.name}" 퀘스트를 달성하여 넙죽코인 ${quest.reward.credit}개를 획득하셨습니다.`, + button: { text: "확인하기", path: "/event/2024spring-missions" }, + }; + toast(notificationValue); + }); + prevEventStatusRef.current = completedQuests; + }, [completedQuests]); +}; diff --git a/packages/web/src/hooks/event/useEvent2024SpringQuestComplete.ts b/packages/web/src/hooks/event/useEvent2024SpringQuestComplete.ts new file mode 100644 index 000000000..45adf4c3c --- /dev/null +++ b/packages/web/src/hooks/event/useEvent2024SpringQuestComplete.ts @@ -0,0 +1,36 @@ +import { useCallback } from "react"; + +import type { QuestId } from "@/types/event2024spring"; + +import { + useFetchRecoilState, + useValueRecoilState, +} from "@/hooks/useFetchRecoilState"; +import { useAxios } from "@/hooks/useTaxiAPI"; + +export const useEvent2024SpringQuestComplete = () => { + const axios = useAxios(); + const fetchEvent2024SpringInfo = useFetchRecoilState("event2024SpringInfo"); + const { completedQuests, quests } = + useValueRecoilState("event2024SpringInfo") || {}; + + return useCallback( + (id: QuestId) => { + if (!completedQuests || !quests) return; + const questMaxCount = + quests?.find((quest) => quest.id === id)?.maxCount || 0; + const questCompletedCount = completedQuests?.filter( + (questId) => questId === id + ).length; + if (questMaxCount > 0 && questCompletedCount >= questMaxCount) return; + if (["roomSharing"].includes(id)) { + axios({ + url: `/events/2024spring/quests/complete/${id}`, + method: "post", + onSuccess: () => fetchEvent2024SpringInfo(), + }); + } else fetchEvent2024SpringInfo(); + }, + [completedQuests, fetchEvent2024SpringInfo, quests] + ); +}; diff --git a/packages/web/src/hooks/event/useEventBackgroundEffect.ts b/packages/web/src/hooks/event/useEventBackgroundEffect.ts new file mode 100644 index 000000000..1543bd0de --- /dev/null +++ b/packages/web/src/hooks/event/useEventBackgroundEffect.ts @@ -0,0 +1,13 @@ +import { CSSProperties, useEffect } from "react"; + +export const useEventBackgroundEffect = ( + color: CSSProperties["color"] = "#000000" +) => { + useEffect(() => { + const prevBackground = document.body.style.background; + document.body.style.background = color; + return () => { + document.body.style.background = prevBackground; + }; + }, [color]); +}; diff --git a/packages/web/src/hooks/useFetchRecoilState/index.tsx b/packages/web/src/hooks/useFetchRecoilState/index.tsx index 33bae30d8..a6701ce67 100644 --- a/packages/web/src/hooks/useFetchRecoilState/index.tsx +++ b/packages/web/src/hooks/useFetchRecoilState/index.tsx @@ -5,6 +5,11 @@ import { useSetEvent2023FallInfo, useValueEvent2023FallInfo, } from "./useFetchEvent2023FallInfo"; +import { + useFetchEvent2024SpringInfo, + useSetEvent2024SpringInfo, + useValueEvent2024SpringInfo, +} from "./useFetchEvent2024SpringInfo"; import { useFetchLoginInfo, useSetLoginInfo, @@ -27,6 +32,7 @@ import { } from "./useFetchTaxiLocations"; import { Event2023FallInfoType } from "@/atoms/event2023FallInfo"; +import { Event2024SpringInfoType } from "@/atoms/event2024SpringInfo"; import { LoginInfoType } from "@/atoms/loginInfo"; import { MyRoomsType } from "@/atoms/myRooms"; import { notificationOptionsType } from "@/atoms/notificationOptions"; @@ -37,7 +43,8 @@ export type AtomName = | "taxiLocations" | "myRooms" | "notificationOptions" - | "event2023FallInfo"; + | "event2023FallInfo" + | "event2024SpringInfo"; type useValueRecoilStateType = { (atomName: "loginInfo"): LoginInfoType; @@ -45,6 +52,7 @@ type useValueRecoilStateType = { (atomName: "myRooms"): MyRoomsType; (atomName: "notificationOptions"): notificationOptionsType; (atomName: "event2023FallInfo"): Event2023FallInfoType; + (atomName: "event2024SpringInfo"): Event2024SpringInfoType; }; const _useValueRecoilState = (atomName: AtomName) => { switch (atomName) { @@ -58,6 +66,8 @@ const _useValueRecoilState = (atomName: AtomName) => { return useValueNotificationOptions(); case "event2023FallInfo": return useValueEvent2023FallInfo(); + case "event2024SpringInfo": + return useValueEvent2024SpringInfo(); } }; export const useValueRecoilState = @@ -75,6 +85,8 @@ export const useSetRecoilState = (atomName: AtomName) => { return useSetNotificationOptions(); case "event2023FallInfo": return useSetEvent2023FallInfo(); + case "event2024SpringInfo": + return useSetEvent2024SpringInfo(); } }; @@ -90,6 +102,8 @@ export const useFetchRecoilState = (atomName: AtomName) => { return useFetchNotificationOptions(); case "event2023FallInfo": return useFetchEvent2023FallInfo(); + case "event2024SpringInfo": + return useFetchEvent2024SpringInfo(); } }; @@ -116,6 +130,10 @@ export const useSyncRecoilStateEffect = () => { // event2023FallInfo 초기화 및 동기화 const fetchEvent2023FallInfo = useFetchRecoilState("event2023FallInfo"); useEffect(fetchEvent2023FallInfo, [userId]); + + // event2024SpringInfo 초기화 및 동기화 + const fetchEvent2024SpringInfo = useFetchRecoilState("event2024SpringInfo"); + useEffect(fetchEvent2024SpringInfo, [userId]); }; export const useIsLogin = (): boolean => { diff --git a/packages/web/src/hooks/useFetchRecoilState/useFetchEvent2024SpringInfo.tsx b/packages/web/src/hooks/useFetchRecoilState/useFetchEvent2024SpringInfo.tsx new file mode 100644 index 000000000..90c6020bb --- /dev/null +++ b/packages/web/src/hooks/useFetchRecoilState/useFetchEvent2024SpringInfo.tsx @@ -0,0 +1,31 @@ +import { useCallback } from "react"; + +import { useAxios } from "@/hooks/useTaxiAPI"; +import { AxiosOption } from "@/hooks/useTaxiAPI/useAxios"; + +import event2024SpringInfoAtom from "@/atoms/event2024SpringInfo"; +import { useRecoilValue, useSetRecoilState } from "recoil"; + +import { eventMode } from "@/tools/loadenv"; + +export const useValueEvent2024SpringInfo = () => + useRecoilValue(event2024SpringInfoAtom); +export const useSetEvent2024SpringInfo = () => + useSetRecoilState(event2024SpringInfoAtom); +export const useFetchEvent2024SpringInfo = () => { + const setEvent2024SpringInfo = useSetEvent2024SpringInfo(); + const axios = useAxios(); + + return useCallback((onError?: AxiosOption["onError"]) => { + if (eventMode === "2024spring") { + axios({ + url: "/events/2024spring/globalState/", + method: "get", + onSuccess: (data) => setEvent2024SpringInfo(data), + onError: onError, + }); + } else { + setEvent2024SpringInfo(null); + } + }, []); +}; diff --git a/packages/web/src/pages/Addroom/index.tsx b/packages/web/src/pages/Addroom/index.tsx index 335a5b2db..4fa1ed1c7 100644 --- a/packages/web/src/pages/Addroom/index.tsx +++ b/packages/web/src/pages/Addroom/index.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useCookies } from "react-cookie"; import { useHistory } from "react-router-dom"; -import { useEvent2023FallQuestComplete } from "@/hooks/event/useEvent2023FallQuestComplete"; +import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; import { useFetchRecoilState, useIsLogin, @@ -12,6 +12,7 @@ import { useAxios } from "@/hooks/useTaxiAPI"; import AdaptiveDiv from "@/components/AdaptiveDiv"; import Button from "@/components/Button"; +import { ModalEvent2024SpringAbuseWarning } from "@/components/ModalPopup"; import { OptionDate, OptionMaxPeople, @@ -61,8 +62,10 @@ const AddRoom = () => { const isLogin = useIsLogin(); const myRooms = useValueRecoilState("myRooms"); const fetchMyRooms = useFetchRecoilState("myRooms"); - //#region event2023Fall - const event2023FallQuestComplete = useEvent2023FallQuestComplete(); + //#region event2024Spring + const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); + const [isOpenModalEventAbuseWarning, setIsOpenModalEventAbuseWarning] = + useState(false); //#endregion useEffect(() => { @@ -103,6 +106,44 @@ const AddRoom = () => { const onClickAdd = async () => { if (!onCall.current) { onCall.current = true; + + // #region event2024Spring + let isAgreeOnTermsOfEvent = false; + await axios({ + url: "/events/2024spring/globalState", + method: "get", + onSuccess: (data) => { + if (data.isAgreeOnTermsOfEvent) { + isAgreeOnTermsOfEvent = data.isAgreeOnTermsOfEvent; + } + }, + onError: () => {}, + }); + if (isAgreeOnTermsOfEvent) { + let isFalse = false; + await axios({ + url: "/rooms/create/test", + method: "post", + data: { + from: valuePlace[0], + to: valuePlace[1], + time: calculatedTime!.toISOString(), + maxPartLength: valueMaxPeople, + }, + onSuccess: (data) => { + if (data!.result === false) { + setIsOpenModalEventAbuseWarning(true); + onCall.current = false; + isFalse = true; + return; + } + }, + onError: () => {}, + }); + if (isFalse) return; + } + // #endregion + // FIXME: "/rooms/create" API가 myRoom을 반환하도록 수정 await axios({ url: "/rooms/create", @@ -116,8 +157,8 @@ const AddRoom = () => { }, onSuccess: () => { fetchMyRooms(); - //#region event2023Fall - event2023FallQuestComplete("firstRoomCreation"); + //#region event2024Spring + event2024SpringQuestComplete("firstRoomCreation"); //#endregion history.push("/myroom"); }, @@ -128,52 +169,85 @@ const AddRoom = () => { }; return (myRooms?.ongoing.length ?? 0) < MAX_PARTICIPATION ? ( -
- - - 방 개설하기 - - {isLogin ? ( - <> - - - - - - - - ) : ( - - )} - -
+ <> +
+ + + 방 개설하기 + + {isLogin ? ( + <> + + + + + + + + ) : ( + + )} + +
+ {/* #region event2024Spring */} + { + if (data === true) { + setIsOpenModalEventAbuseWarning(data); + await axios({ + url: "/rooms/create", + method: "post", + data: { + name: valueName || randomRoomName, + from: valuePlace[0], + to: valuePlace[1], + time: calculatedTime!.toISOString(), + maxPartLength: valueMaxPeople, + }, + onSuccess: () => { + fetchMyRooms(); + //#region event2024spring + event2024SpringQuestComplete("firstRoomCreation"); + //#endregion + history.push("/myroom"); + }, + onError: () => setAlert("방 개설에 실패하였습니다."), + }); + } else if (data === false) { + setIsOpenModalEventAbuseWarning(data); + } + }} + /> + {/* #endregion */} + ) : ( ); diff --git a/packages/web/src/pages/Event/Event2024Spring.tsx b/packages/web/src/pages/Event/Event2024Spring.tsx new file mode 100644 index 000000000..b5c25439c --- /dev/null +++ b/packages/web/src/pages/Event/Event2024Spring.tsx @@ -0,0 +1,399 @@ +import { ReactNode, memo, useCallback, useEffect, useState } from "react"; +import { Link } from "react-router-dom"; + +import { QuestId } from "@/types/event2024spring"; + +import { useEventBackgroundEffect } from "@/hooks/event/useEventBackgroundEffect"; +import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; +import { useAxios } from "@/hooks/useTaxiAPI"; + +import AdaptiveDiv from "@/components/AdaptiveDiv"; +import EventButton from "@/components/Event/EventButton"; +import Footer from "@/components/Footer"; +import HeaderWithBackButton from "@/components/Header/HeaderWithBackButton"; +import { ModalEvent2024SpringShare } from "@/components/ModalPopup"; + +import { MissionContainer } from "./Event2024SpringMissions"; + +import alertAtom from "@/atoms/alert"; +import { useSetRecoilState } from "recoil"; + +import eventTheme from "@/tools/eventTheme"; +import theme from "@/tools/theme"; + +import AIArt from "@/static/events/2024springAIArt.png"; +import GoldPrize from "@/static/events/2024springGoldPrize.png"; +import LineArt from "@/static/events/2024springLineArt.png"; +import NupzukCoin from "@/static/events/2024springNubzukcoinLarge.gif"; +import NupzukiEyes from "@/static/events/2024springNubzukiEyes.png"; +import SilverPrize from "@/static/events/2024springSilverPrize.png"; +import { ReactComponent as MainTitle } from "@/static/events/2024springTitle.svg"; + +type EventStepProps = { + step: string; + title: string; + subtitle?: string; + children?: ReactNode; +}; + +const EventStep = ({ + step, + title, + subtitle = "", + children, +}: EventStepProps) => { + return ( +
+
+ {step} +
+ {title} +

+ {subtitle} +
+
+ {children} +
+ ); +}; + +const Event2024Spring = () => { + const [isOpenShare, setIsOpenShare] = useState(false); + const [inviteUrl, setInviteUrl] = useState(); + const setAlert = useSetRecoilState(alertAtom); + const { isAgreeOnTermsOfEvent } = + useValueRecoilState("event2024SpringInfo") || {}; + + const axios = useAxios(); + + const getInviteUrl = useCallback( + () => + axios({ + url: `/events/2024spring/invite/create`, + method: "post", + onSuccess: ({ inviteUrl }) => { + setInviteUrl(inviteUrl); + }, + onError: () => setAlert("공유 링크를 생성하지 못했습니다."), + }), + [isAgreeOnTermsOfEvent] + ); + + useEffect(() => { + if (isAgreeOnTermsOfEvent) getInviteUrl(); + }, [isAgreeOnTermsOfEvent]); + + const styleTextBox = { + ...eventTheme.font20, + display: "flex", + padding: "24px 16px", + justifyContent: "center", + alignItems: "center", + gap: "10px", + border: "4px solid #000", + background: "#FFF", + } as const; + const styleVerticalCenter = { + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + } as const; + + const styleAdaptiveDiv = { + ...styleVerticalCenter, + width: "100%", + margin: 0, + } as const; + + const exampleMission = { + name: "첫 발걸음", + description: + "로그인만 해도 넙죽코인을 얻을 수 있다고?? 이벤트 기간에 처음으로 SPARCS Taxi 서비스에 로그인하여 넙죽코인을 받아보세요.", + imageUrl: + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_firstLogin.png", + reward: { + credit: 50, + ticket1: 0, + }, + id: "firstLogin" as QuestId, + maxCount: 1, + }; + + useEventBackgroundEffect(); + + return ( + <> + +
이벤트 안내
+
+ + +
+ 2024.02.23. ~ 03.18. +
+ line art + { + if (inviteUrl) setIsOpenShare(true); + else + setAlert("이벤트를 공유하기 위해서는 이벤트에 참여해야 합니다."); + }} + /> + +
+ + Nupzuki Eyes +
+ + KAIST + + 24학번 새내기들이여... +
+
+ +
+
+ 아주 오래전부터 +
+ AI Art of a taxi +
+ KAIST 학생들은{" "} + + 택시 + + 를 타고 곳곳을 모험했습니다. +
+
+
+ + 이제 여러분들의 차례입니다! + Nupzuk Coin + + + 이벤트 참여 방법

↓ +
+ + +
+ +
+ + + +
+ +
+
+ + 1등 새터반 + + + 2등 새터반 + +
+
+ prize + prize +
+
+ + + +
+ + + + + +
+