diff --git a/admin/.gitignore b/admin/.gitignore index a547bf36..3b0b4037 100644 --- a/admin/.gitignore +++ b/admin/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env \ No newline at end of file diff --git a/client/src/apis/linkAPI.ts b/client/src/apis/linkAPI.ts new file mode 100644 index 00000000..822ec7b4 --- /dev/null +++ b/client/src/apis/linkAPI.ts @@ -0,0 +1,22 @@ +import { GetShareLinkResponse } from "@/types/linkApi"; +import { fetchWithTimeout } from "@/utils/fetchWithTimeout"; + +const baseURL = `${import.meta.env.VITE_API_URL}/link`; +const headers = { + "Content-Type": "application/json", +}; + +export const LinkAPI = { + async getShareLink(token: string): Promise { + try { + const response = await fetchWithTimeout(`${baseURL}`, { + method: "POST", + headers: { ...headers, Authorization: `Bearer ${token}` }, + }); + return response.json(); + } catch (error) { + console.error("Error:", error); + throw error; + } + }, +}; diff --git a/client/src/constants/Auth/token.ts b/client/src/constants/Auth/token.ts deleted file mode 100644 index c8073010..00000000 --- a/client/src/constants/Auth/token.ts +++ /dev/null @@ -1 +0,0 @@ -export const COOKIE_TOKEN_KEY = "token"; diff --git a/client/src/constants/cookie.ts b/client/src/constants/cookie.ts new file mode 100644 index 00000000..7b427b53 --- /dev/null +++ b/client/src/constants/cookie.ts @@ -0,0 +1,4 @@ +export const COOKIE_KEY = { + ACCESS_TOKEN: "token", + INVITE_USER: "referrerId", +} as const; diff --git a/client/src/features/CasperCustom/CasperCustomFinish.tsx b/client/src/features/CasperCustom/CasperCustomFinish.tsx index a3abacd5..d64905a6 100644 --- a/client/src/features/CasperCustom/CasperCustomFinish.tsx +++ b/client/src/features/CasperCustom/CasperCustomFinish.tsx @@ -2,14 +2,16 @@ import { useEffect, useRef } from "react"; import { motion } from "framer-motion"; import { useCookies } from "react-cookie"; import { Link } from "react-router-dom"; +import { LinkAPI } from "@/apis/linkAPI"; import { LotteryAPI } from "@/apis/lotteryAPI"; import CTAButton from "@/components/CTAButton"; -import { COOKIE_TOKEN_KEY } from "@/constants/Auth/token"; import { MAX_APPLY } from "@/constants/CasperCustom/customStep"; import { DISSOLVE } from "@/constants/animation"; +import { COOKIE_KEY } from "@/constants/cookie"; import useCasperCustomDispatchContext from "@/hooks/useCasperCustomDispatchContext"; import useCasperCustomStateContext from "@/hooks/useCasperCustomStateContext"; import useFetch from "@/hooks/useFetch"; +import useToast from "@/hooks/useToast"; import { CASPER_ACTION } from "@/types/casperCustom"; import { GetApplyCountResponse } from "@/types/lotteryApi"; import { saveDomImage } from "@/utils/saveDomImage"; @@ -27,10 +29,11 @@ export function CasperCustomFinish({ handleResetStep, unblockNavigation, }: CasperCustomFinishProps) { - const [cookies] = useCookies([COOKIE_TOKEN_KEY]); + const [cookies] = useCookies([COOKIE_KEY.ACCESS_TOKEN]); + const { showToast, ToastComponent } = useToast("링크가 복사되었어요!"); const { data: applyCountData, fetchData: getApplyCount } = useFetch(() => - LotteryAPI.getApplyCount(cookies[COOKIE_TOKEN_KEY]) + LotteryAPI.getApplyCount(cookies[COOKIE_KEY.ACCESS_TOKEN]) ); const dispatch = useCasperCustomDispatchContext(); @@ -39,7 +42,7 @@ export function CasperCustomFinish({ const casperCustomRef = useRef(null); useEffect(() => { - if (!cookies[COOKIE_TOKEN_KEY]) { + if (!cookies[COOKIE_KEY.ACCESS_TOKEN]) { return; } @@ -60,6 +63,17 @@ export function CasperCustomFinish({ dispatch({ type: CASPER_ACTION.RESET_CUSTOM }); }; + const handleClickShareButton = async () => { + const link = await LinkAPI.getShareLink(cookies[COOKIE_KEY.ACCESS_TOKEN]); + + try { + await navigator.clipboard.writeText(link.shortenLocalUrl); + showToast(); + } catch (err) { + console.error("Failed to copy: ", err); + } + }; + return (
@@ -102,7 +116,10 @@ export function CasperCustomFinish({
)} - + @@ -112,6 +129,8 @@ export function CasperCustomFinish({

+ + {ToastComponent}
); } diff --git a/client/src/features/CasperCustom/CasperCustomForm.tsx b/client/src/features/CasperCustom/CasperCustomForm.tsx index 086b3c8c..fd15a037 100644 --- a/client/src/features/CasperCustom/CasperCustomForm.tsx +++ b/client/src/features/CasperCustom/CasperCustomForm.tsx @@ -4,9 +4,9 @@ import { useCookies } from "react-cookie"; import { LotteryAPI } from "@/apis/lotteryAPI"; import CTAButton from "@/components/CTAButton"; import TextField from "@/components/TextField"; -import { COOKIE_TOKEN_KEY } from "@/constants/Auth/token"; import { CUSTOM_OPTION } from "@/constants/CasperCustom/casper"; import { DISSOLVE } from "@/constants/animation"; +import { COOKIE_KEY } from "@/constants/cookie"; import useCasperCustomDispatchContext from "@/hooks/useCasperCustomDispatchContext"; import useCasperCustomStateContext from "@/hooks/useCasperCustomStateContext"; import useFetch from "@/hooks/useFetch"; @@ -20,14 +20,17 @@ interface CasperCustomFormProps { } export function CasperCustomForm({ navigateNextStep }: CasperCustomFormProps) { - const [cookies] = useCookies([COOKIE_TOKEN_KEY]); + const [cookies] = useCookies([COOKIE_KEY.ACCESS_TOKEN, COOKIE_KEY.INVITE_USER]); const { data: casper, isSuccess: isSuccessPostCasper, fetchData: postCasper, - } = useFetch( - ({ token, casper }) => LotteryAPI.postCasper(token, casper) + } = useFetch< + PostCasperResponse, + { token: string; referrerId: string; casper: CasperInformationType } + >(({ token, referrerId, casper }) => + LotteryAPI.postCasper(token, { ...casper, [COOKIE_KEY.INVITE_USER]: referrerId }) ); const { casperName, expectations, selectedCasperIdx } = useCasperCustomStateContext(); @@ -80,7 +83,11 @@ export function CasperCustomForm({ navigateNextStep }: CasperCustomFormProps) { expectation: expectations, }; - await postCasper({ token: cookies[COOKIE_TOKEN_KEY], casper }); + await postCasper({ + token: cookies[COOKIE_KEY.ACCESS_TOKEN], + referrerId: cookies[COOKIE_KEY.INVITE_USER], + casper, + }); }; return ( diff --git a/client/src/pages/Lottery/index.tsx b/client/src/pages/Lottery/index.tsx index 448b29df..88d0dc4b 100644 --- a/client/src/pages/Lottery/index.tsx +++ b/client/src/pages/Lottery/index.tsx @@ -1,11 +1,11 @@ import { useCallback, useEffect, useState } from "react"; import { useCookies } from "react-cookie"; -import { useLoaderData, useNavigate } from "react-router-dom"; +import { useLoaderData, useLocation, useNavigate } from "react-router-dom"; import { AuthAPI } from "@/apis/authAPI"; import Footer from "@/components/Footer"; import Notice from "@/components/Notice"; -import { COOKIE_TOKEN_KEY } from "@/constants/Auth/token"; import { LOTTERY_SECTIONS } from "@/constants/PageSections/sections.ts"; +import { COOKIE_KEY } from "@/constants/cookie"; import { CustomDesign, HeadLamp, @@ -33,11 +33,16 @@ export default function Lottery() { useScrollTop(); const navigate = useNavigate(); + const location = useLocation(); + const queryParams = new URLSearchParams(location.search); + const inviteUser = queryParams.get(COOKIE_KEY.INVITE_USER); + + const lotteryData = useLoaderData() as GetLotteryResponse; const containerRef = useHeaderStyleObserver({ darkSections: [LOTTERY_SECTIONS.HEADLINE, LOTTERY_SECTIONS.SHORT_CUT], }); - const [_cookies, setCookie] = useCookies([COOKIE_TOKEN_KEY]); + const [_cookies, setCookie] = useCookies([COOKIE_KEY.ACCESS_TOKEN, COOKIE_KEY.INVITE_USER]); const { data: authToken, @@ -52,11 +57,14 @@ export default function Lottery() { const [phoneNumberState, setPhoneNumberState] = useState(phoneNumber); - const lotteryData = useLoaderData() as GetLotteryResponse; - + useEffect(() => { + if (inviteUser) { + setCookie(COOKIE_KEY.INVITE_USER, inviteUser); + } + }, [inviteUser]); useEffect(() => { if (authToken && isSuccessGetAuthToken) { - setCookie(COOKIE_TOKEN_KEY, authToken.accessToken); + setCookie(COOKIE_KEY.ACCESS_TOKEN, authToken.accessToken); dispatch({ type: PHONE_NUMBER_ACTION.SET_PHONE_NUMBER, payload: phoneNumberState }); navigate("/lottery/custom"); } diff --git a/client/src/types/linkApi.ts b/client/src/types/linkApi.ts new file mode 100644 index 00000000..027e56af --- /dev/null +++ b/client/src/types/linkApi.ts @@ -0,0 +1,4 @@ +export interface GetShareLinkResponse { + shortenUrl: string; + shortenLocalUrl: string; +} diff --git a/client/src/types/lotteryApi.ts b/client/src/types/lotteryApi.ts index 55b5253d..6e9d2c4f 100644 --- a/client/src/types/lotteryApi.ts +++ b/client/src/types/lotteryApi.ts @@ -1,3 +1,5 @@ +import { COOKIE_KEY } from "@/constants/cookie"; + export interface CasperInformationType { eyeShape: number; eyePosition: number; @@ -12,7 +14,9 @@ export type GetCasperListResponse = ({ casperId: number; } & CasperInformationType)[]; -export interface PostCasperRequestBody extends CasperInformationType {} +export interface PostCasperRequestBody extends CasperInformationType { + [COOKIE_KEY.INVITE_USER]: string; +} export interface PostCasperResponse extends CasperInformationType { casperId: number;