diff --git a/packages/service/src/App.tsx b/packages/service/src/App.tsx index 827fb1ba..1850cc69 100644 --- a/packages/service/src/App.tsx +++ b/packages/service/src/App.tsx @@ -9,8 +9,8 @@ import { ErrorBoundary } from "react-error-boundary"; export const App = () => { return (
- + }> diff --git a/packages/service/src/apis/partsEvent/apiGetSharedParts.ts b/packages/service/src/apis/partsEvent/apiGetSharedParts.ts new file mode 100644 index 00000000..233aa149 --- /dev/null +++ b/packages/service/src/apis/partsEvent/apiGetSharedParts.ts @@ -0,0 +1,8 @@ +import { IMyParts } from "@watermelon-clap/core/src/types"; +import { customFetch } from "@watermelon-clap/core/src/utils"; + +export const apiGetSharedParts = (link_key?: string): Promise => { + return customFetch( + `${import.meta.env.VITE_BACK_BASE_URL}/event/parts/link/${link_key}`, + ).then((res) => res.json()); +}; diff --git a/packages/service/src/common/components/Button/Button.css.ts b/packages/service/src/common/components/Button/Button.css.ts index d44ed4ee..1c455fb9 100644 --- a/packages/service/src/common/components/Button/Button.css.ts +++ b/packages/service/src/common/components/Button/Button.css.ts @@ -4,7 +4,7 @@ import { theme } from "@watermelon-clap/core/src/theme"; export const longButtonStyle = css` ${theme.flex.center} ${theme.font.preM20} - background: ${theme.color.black}; + background: ${theme.color.eventBlue}; color: ${theme.color.white}; width: 360px; height: 70px; @@ -15,11 +15,10 @@ export const longButtonStyle = css` border: none; &:hover { - opacity: 0.8; + opacity: 0.9; } &:active { - background: ${theme.color.black}; - color: ${theme.color.gray400}; + background: ${theme.color.eventSkyblue}; } `; diff --git a/packages/service/src/common/components/GlobalNavigationBar/GlobalNavigationBar.tsx b/packages/service/src/common/components/GlobalNavigationBar/GlobalNavigationBar.tsx index 60ef83ce..b04e7aa7 100644 --- a/packages/service/src/common/components/GlobalNavigationBar/GlobalNavigationBar.tsx +++ b/packages/service/src/common/components/GlobalNavigationBar/GlobalNavigationBar.tsx @@ -7,9 +7,11 @@ import { useState } from "react"; import { MenuButton } from "./MenuButton"; import { useMobile } from "@service/common/hooks/useMobile"; import { GNB_BREAKPOINT } from "@service/constants/breakpoints"; +import { useErrorBoundary } from "react-error-boundary"; export const GlobalNavigationBar = () => { const [isOpen, setIsOpen] = useState(false); + const { resetBoundary } = useErrorBoundary(); // 800px보다 작아지면 햄버거 메뉴 생성 const isMobile = useMobile(GNB_BREAKPOINT); @@ -21,6 +23,7 @@ export const GlobalNavigationBar = () => { css={logoStyles} onClick={() => { navigate(MAIN_PAGE_ROUTE); + resetBoundary(); }} /> diff --git a/packages/service/src/common/components/GlobalNavigationBar/GlobalNavs/GlobalNavs.css.ts b/packages/service/src/common/components/GlobalNavigationBar/GlobalNavs/GlobalNavs.css.ts index 0be2e729..ca2a28ea 100644 --- a/packages/service/src/common/components/GlobalNavigationBar/GlobalNavs/GlobalNavs.css.ts +++ b/packages/service/src/common/components/GlobalNavigationBar/GlobalNavs/GlobalNavs.css.ts @@ -43,6 +43,7 @@ export const linkStyles = css` display: inline-flex; gap: 4px; flex-shrink: 0; + cursor: pointer; &:hover { transform: translateY(-1px); diff --git a/packages/service/src/common/components/GlobalNavigationBar/GlobalNavs/GlobalNavs.tsx b/packages/service/src/common/components/GlobalNavigationBar/GlobalNavs/GlobalNavs.tsx index 664f9058..aeef5bb0 100644 --- a/packages/service/src/common/components/GlobalNavigationBar/GlobalNavs/GlobalNavs.tsx +++ b/packages/service/src/common/components/GlobalNavigationBar/GlobalNavs/GlobalNavs.tsx @@ -1,6 +1,7 @@ -import { Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { ReactComponent as NLogo } from "public/images/gnb/n-logo.svg"; import { linkStyles, navsContainerStyles, nLogoStyles } from "./GlobalNavs.css"; +import { useErrorBoundary } from "react-error-boundary"; import { NEW_CAR_PAGE_ROUTE, PICK_EVENT_PAGE_ROUTE, @@ -9,23 +10,43 @@ import { } from "@service/constants/routes"; const GlobalNavs = ({ isOpen }: { isOpen: boolean }) => { + const navigate = useNavigate(); + const { resetBoundary } = useErrorBoundary(); + + const handleNavigation = (route: string) => { + navigate(route); + resetBoundary(); + }; + return (
- +
handleNavigation(NEW_CAR_PAGE_ROUTE)} + > 아반떼 N - - +
+
handleNavigation(PICK_EVENT_PAGE_ROUTE)} + > 내 아반떼 N 뽑기 - - +
+
handleNavigation(N_QUIZ_EVENT_PAGE_ROUTE)} + > 퀴즈 - - +
+
handleNavigation(PARTS_COLLECTION_PAGE_ROUTE)} + > 내 컬렉션 - - +
+
handleNavigation("#")}> 응모 내역 확인 - +
); }; diff --git a/packages/service/src/components/partsCollection/CustomCard/CustomCard.css.ts b/packages/service/src/components/partsCollection/CustomCard/CustomCard.css.ts index 552b4643..d1328e1f 100644 --- a/packages/service/src/components/partsCollection/CustomCard/CustomCard.css.ts +++ b/packages/service/src/components/partsCollection/CustomCard/CustomCard.css.ts @@ -20,6 +20,8 @@ export const bgImg = css` top: 0; left: 0; z-index: 1; + border-radius: 20px; + height: 100%; ${mobile(css` width: 400px; diff --git a/packages/service/src/components/partsCollection/CustomCard/CustomCard.tsx b/packages/service/src/components/partsCollection/CustomCard/CustomCard.tsx index 9254c222..9ff05337 100644 --- a/packages/service/src/components/partsCollection/CustomCard/CustomCard.tsx +++ b/packages/service/src/components/partsCollection/CustomCard/CustomCard.tsx @@ -17,7 +17,7 @@ export const CustomCard = ({
- + {bgImg && } diff --git a/packages/service/src/components/share/CustomCard/CustomCard.css.ts b/packages/service/src/components/share/CustomCard/CustomCard.css.ts new file mode 100644 index 00000000..d1328e1f --- /dev/null +++ b/packages/service/src/components/share/CustomCard/CustomCard.css.ts @@ -0,0 +1,86 @@ +import { css } from "@emotion/react"; +import { mobile } from "@service/common/responsive/responsive"; + +export const card = css` + width: 700px; + aspect-ratio: 16 / 9; + border-radius: 20px; + background-color: white; + margin: 0 auto; + position: relative; + + ${mobile(css` + width: 400px; + `)} +`; + +export const bgImg = css` + position: absolute; + width: 700px; + top: 0; + left: 0; + z-index: 1; + border-radius: 20px; + height: 100%; + + ${mobile(css` + width: 400px; + `)} +`; + +export const carImg = css` + width: 700px; + z-index: 2; + position: absolute; + + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + ${mobile(css` + width: 450px; + `)} +`; + +export const colorImg = css` + width: 700px; + z-index: 2; + position: absolute; + + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + ${mobile(css` + width: 450px; + `)} +`; + +export const wheelImg = css` + z-index: 3; + + position: absolute; + width: 245px; + bottom: 95px; + left: 305px; + + ${mobile(css` + bottom: 45px; + left: 170px; + width: 161px; + `)} +`; + +export const spoilerImg = css` + z-index: 3; + position: absolute; + width: 50px; + top: 144px; + right: 118px; + + ${mobile(css` + width: 30px; + top: 78px; + right: 53px; + `)} +`; diff --git a/packages/service/src/components/share/CustomCard/CustomCard.tsx b/packages/service/src/components/share/CustomCard/CustomCard.tsx new file mode 100644 index 00000000..51518151 --- /dev/null +++ b/packages/service/src/components/share/CustomCard/CustomCard.tsx @@ -0,0 +1,26 @@ +import * as style from "./CustomCard.css"; + +export interface ICustomCardProps { + bgImg?: string; + spoilerImg?: string; + wheelImg?: string; + colorImg?: string; +} + +export const CustomCard = ({ + bgImg, + spoilerImg, + wheelImg, + colorImg, +}: ICustomCardProps) => { + return ( +
+ + + {bgImg && } + + + +
+ ); +}; diff --git a/packages/service/src/components/share/CustomCard/index.ts b/packages/service/src/components/share/CustomCard/index.ts new file mode 100644 index 00000000..f085d78f --- /dev/null +++ b/packages/service/src/components/share/CustomCard/index.ts @@ -0,0 +1 @@ +export { CustomCard } from "./CustomCard"; diff --git a/packages/service/src/components/share/PartsTab/PartsCard/PartsCard.css.ts b/packages/service/src/components/share/PartsTab/PartsCard/PartsCard.css.ts new file mode 100644 index 00000000..92507ac5 --- /dev/null +++ b/packages/service/src/components/share/PartsTab/PartsCard/PartsCard.css.ts @@ -0,0 +1,37 @@ +import { css } from "@emotion/react"; +import { mobile } from "@service/common/responsive/responsive"; +import { theme } from "@watermelon-clap/core/src/theme"; + +export const container = css` + ${theme.flex.column} + width : 20%; + flex-shrink: 1; + + ${mobile(css` + width: 30%; + margin: 0 4%; + `)} +`; + +export const card = css` + border-radius: 20px; + aspect-ratio: 1 / 1; + overflow: hidden; + position: relative; + background-color: ${theme.color.white}; + ${theme.flex.center}; +`; + +export const img = (cate: string) => css` + width: 100%; + width: ${cate === "REAR" && "80%"}; + ${cate === "DRIVE_MODE" && + "height : 80%; width : 80%; border-radius : 10px; object-fit : cover"}; +`; + +export const name = css` + ${theme.font.preB}; + font-size: clamp(0px, calc(10px + 1vw), 24px); + color: ${theme.color.white}; + margin-top: 20px; +`; diff --git a/packages/service/src/components/share/PartsTab/PartsCard/PartsCard.tsx b/packages/service/src/components/share/PartsTab/PartsCard/PartsCard.tsx new file mode 100644 index 00000000..d6d1cc46 --- /dev/null +++ b/packages/service/src/components/share/PartsTab/PartsCard/PartsCard.tsx @@ -0,0 +1,20 @@ +import * as style from "./PartsCard.css"; +import { IParts } from "@watermelon-clap/core/src/types"; + +interface IPartsCardProps { + partsData: IParts; +} + +export const PartsCard = ({ partsData }: IPartsCardProps) => { + return ( +
+
+ +
+ {partsData.name} +
+ ); +}; diff --git a/packages/service/src/components/share/PartsTab/PartsCard/index.ts b/packages/service/src/components/share/PartsTab/PartsCard/index.ts new file mode 100644 index 00000000..396dab49 --- /dev/null +++ b/packages/service/src/components/share/PartsTab/PartsCard/index.ts @@ -0,0 +1 @@ +export { PartsCard } from "./PartsCard"; diff --git a/packages/service/src/components/share/PartsTab/PartsWrap.css.ts b/packages/service/src/components/share/PartsTab/PartsWrap.css.ts new file mode 100644 index 00000000..090d7be5 --- /dev/null +++ b/packages/service/src/components/share/PartsTab/PartsWrap.css.ts @@ -0,0 +1,54 @@ +import { css } from "@emotion/react"; +import { mobile } from "@service/common/responsive/responsive"; +import { theme } from "@watermelon-clap/core/src/theme"; + +export const container = css` + ${theme.flex.column} + margin: 0 auto; +`; +export const tabWrap = css` + display: flex; + justify-content: space-between; + margin: 0 auto; + margin-top: 80px; + width: 700px; + + ${mobile(css` + width: 100%; + flex-wrap: wrap; + + justify-content: space-around; + `)} +`; + +export const tabBtn = (isSelected: boolean) => css` + border: ${isSelected ? `2px solid ${theme.color.white}` : `none`}; + ${theme.font.preB}; + font-size: 20px; + border-radius: 100px; + width: 130px; + height: 48px; + + cursor: pointer; + background: none; + color: ${isSelected ? theme.color.white : theme.color.gray300}; + outline: none; + + ${mobile(css` + margin: 10px 8%; + `)} +`; + +export const partsCardWrap = css` + ${theme.flex.between} + gap: 80px 20px; + flex-wrap: wrap; + width: 90%; + max-width: 1000px; + margin-top: 100px; + + ${mobile(css` + justify-content: center; + gap: 20px; + `)} +`; diff --git a/packages/service/src/components/share/PartsTab/PartsWrap.tsx b/packages/service/src/components/share/PartsTab/PartsWrap.tsx new file mode 100644 index 00000000..ddb2354d --- /dev/null +++ b/packages/service/src/components/share/PartsTab/PartsWrap.tsx @@ -0,0 +1,19 @@ +import * as style from "./PartsWrap.css"; +import { PartsCard } from "./PartsCard"; +import { IParts } from "@watermelon-clap/core/src/types"; + +export interface IPartsWrapProps { + equippedPartsData?: IParts[]; +} + +export const PartsWrap = ({ equippedPartsData }: IPartsWrapProps) => { + return ( +
+
+ {equippedPartsData?.map((partsData, idx) => ( + + ))} +
+
+ ); +}; diff --git a/packages/service/src/components/share/PartsTab/index.ts b/packages/service/src/components/share/PartsTab/index.ts new file mode 100644 index 00000000..f35538c0 --- /dev/null +++ b/packages/service/src/components/share/PartsTab/index.ts @@ -0,0 +1 @@ +export { PartsWrap } from "./PartsWrap"; diff --git a/packages/service/src/components/share/index.ts b/packages/service/src/components/share/index.ts new file mode 100644 index 00000000..54b53a41 --- /dev/null +++ b/packages/service/src/components/share/index.ts @@ -0,0 +1 @@ +export * from "./CustomCard"; diff --git a/packages/service/src/constants/routes.ts b/packages/service/src/constants/routes.ts index 14ce8a65..e316236d 100644 --- a/packages/service/src/constants/routes.ts +++ b/packages/service/src/constants/routes.ts @@ -13,5 +13,6 @@ export const NEW_CAR_PAGE_ROUTE = "/new-car" as const; export const N_PARTS_PICK_PAGE_ROUTE = "/parts-pick" as const; export const PICK_EVENT_PAGE_ROUTE = "/pick-event" as const; export const PARTS_COLLECTION_PAGE_ROUTE = "/parts-collection" as const; +export const SHARE_PAGE_ROUTE = "/share/:linkKey" as const; export const N_QUIZ_EVENT_PAGE_WINNER_APLLY_PAGE_ROUTE = "/quiz-event-apply" as const; diff --git a/packages/service/src/pages/Error/Error.tsx b/packages/service/src/pages/Error/Error.tsx index 202560c0..28d7586e 100644 --- a/packages/service/src/pages/Error/Error.tsx +++ b/packages/service/src/pages/Error/Error.tsx @@ -3,9 +3,12 @@ import { errorContainerStyle, errorMessageStyle } from "./Error.css"; import { Button } from "@service/common/components/Button"; import { theme } from "@watermelon-clap/core/src/theme"; import { useNavigate } from "react-router-dom"; +import { useErrorBoundary } from "react-error-boundary"; +import { GlobalNavigationBar } from "@service/common/components/GlobalNavigationBar"; export const Error = ({ error, resetErrorBoundary }: FallbackProps) => { const navigate = useNavigate(); + const { resetBoundary } = useErrorBoundary(); let errorMessage; switch (error.message) { @@ -29,16 +32,19 @@ export const Error = ({ error, resetErrorBoundary }: FallbackProps) => { const handleHomeRedirect = () => { navigate("/"); - window.location.reload(); + resetBoundary(); }; return ( -
-
{errorMessage}
-
- - + <> + +
+
{errorMessage}
+
+ + +
-
+ ); }; diff --git a/packages/service/src/pages/PartsCollection/PartsCollection.css.ts b/packages/service/src/pages/PartsCollection/PartsCollection.css.ts index 8de7dd51..cb6cb1ec 100644 --- a/packages/service/src/pages/PartsCollection/PartsCollection.css.ts +++ b/packages/service/src/pages/PartsCollection/PartsCollection.css.ts @@ -3,7 +3,7 @@ import { mobile } from "@service/common/responsive/responsive"; import { theme } from "@watermelon-clap/core/src/theme"; export const mainBg = css` - background-image: url("images/common/main-bg.webp"); + background-image: url("/images/common/main-bg.webp"); background-size: cover; padding-bottom: 200px; `; diff --git a/packages/service/src/pages/Share/Share.css.ts b/packages/service/src/pages/Share/Share.css.ts new file mode 100644 index 00000000..dea7fb8c --- /dev/null +++ b/packages/service/src/pages/Share/Share.css.ts @@ -0,0 +1,30 @@ +import { css } from "@emotion/react"; +import { mobile } from "@service/common/responsive/responsive"; +import { theme } from "@watermelon-clap/core/src/theme"; + +export const mainBg = css` + background-image: url("/images/common/main-bg.webp"); + background-size: cover; + padding-bottom: 200px; +`; + +export const pageTitle = css` + text-align: center; + ${theme.font.pcpB} + font-size : calc(50px + 2vw); + padding: 100px; + color: ${theme.color.white}; + + ${mobile(css` + font-size: calc(20px + 2vw); + padding: 100px 0 50px 0; + `)} +`; + +export const btn = css` + margin: 0 auto; + ${mobile(css` + width: fit-content; + padding: 10px 20px; + `)} +`; diff --git a/packages/service/src/pages/Share/Share.tsx b/packages/service/src/pages/Share/Share.tsx new file mode 100644 index 00000000..7bfe8557 --- /dev/null +++ b/packages/service/src/pages/Share/Share.tsx @@ -0,0 +1,82 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { IMyParts, IParts } from "@watermelon-clap/core/src/types"; +import { getAccessToken } from "@watermelon-clap/core/src/utils"; +import { useState, useEffect } from "react"; +import * as style from "./Share.css"; +import { PartsWrap } from "@service/components/share/PartsTab"; +import { + CustomCard, + ICustomCardProps, +} from "@service/components/share/CustomCard/CustomCard"; +import { useNavigate, useParams } from "react-router-dom"; +import { apiGetSharedParts } from "@service/apis/partsEvent/apiGetSharedParts"; +import { Button, ButtonVariant } from "@service/common/components/Button"; +import { Space } from "@service/common/styles/Space"; +import { MAIN_PAGE_ROUTE } from "@service/constants/routes"; + +export const Share = () => { + const navigator = useNavigate(); + + const [equippedPartsImg, setEquippedPartsImg] = useState(); + const [equippedPartsData, setEquippedPartsData] = useState(); + + const { linkKey } = useParams(); + + const { data: partsDatas } = useSuspenseQuery({ + queryKey: ["myParts", getAccessToken()], + queryFn: () => apiGetSharedParts(linkKey), + }); + + useEffect(() => { + setEquippedPartsImg(getEquippedParts(partsDatas)._equippedPartsImg); + setEquippedPartsData(getEquippedParts(partsDatas)._equippedPartsData); + }, [partsDatas]); + + return ( +
+

아반떼 N 파츠 컬렉션

+ + + + +
+ ); +}; + +const getEquippedParts = (partsDatas?: IMyParts[]) => { + const _equippedPartsImg: ICustomCardProps = {}; + const _equippedPartsData: IParts[] = []; + + partsDatas?.map((cate) => + cate.parts.map((parts) => { + if (parts.equipped) { + switch (parts.category) { + case "DRIVE_MODE": + _equippedPartsImg.bgImg = parts.imgSrc; + break; + case "COLOR": + _equippedPartsImg.colorImg = parts.imgSrc; + break; + case "REAR": + _equippedPartsImg.spoilerImg = parts.imgSrc; + break; + case "WHEEL": + _equippedPartsImg.wheelImg = parts.imgSrc; + break; + } + _equippedPartsData.push(parts); + } + }), + ); + + return { + _equippedPartsImg: _equippedPartsImg, + _equippedPartsData: _equippedPartsData, + }; +}; diff --git a/packages/service/src/pages/Share/index.ts b/packages/service/src/pages/Share/index.ts new file mode 100644 index 00000000..ebca8e78 --- /dev/null +++ b/packages/service/src/pages/Share/index.ts @@ -0,0 +1 @@ +export { Share } from "./Share"; diff --git a/packages/service/src/pages/index.ts b/packages/service/src/pages/index.ts index 0230fa7c..1621efdf 100644 --- a/packages/service/src/pages/index.ts +++ b/packages/service/src/pages/index.ts @@ -4,4 +4,5 @@ export { NewCar } from "./NewCar"; export { PartsCollection } from "./PartsCollection"; export { PartsPick } from "./PartsPick"; export { PickEvent } from "./PickEvent"; +export { Share } from "./Share"; export { NQuizEventWinnerApply } from "./NQuizEventWinnerApply"; diff --git a/packages/service/src/router.tsx b/packages/service/src/router.tsx index c15845d8..58206498 100644 --- a/packages/service/src/router.tsx +++ b/packages/service/src/router.tsx @@ -14,6 +14,7 @@ import { PICK_EVENT_PAGE_ROUTE, N_PARTS_PICK_PAGE_ROUTE, PARTS_COLLECTION_PAGE_ROUTE, + SHARE_PAGE_ROUTE, N_QUIZ_EVENT_PAGE_WINNER_APLLY_PAGE_ROUTE, } from "./constants/routes"; import { RotateDemoPage } from "./Demo/pages/RotateDemoPage"; @@ -32,6 +33,7 @@ import { NewCar, PartsPick, PartsCollection, + Share, NQuizEventWinnerApply, } from "./pages"; @@ -46,6 +48,7 @@ export const router = createBrowserRouter([ { path: NEW_CAR_PAGE_ROUTE, element: }, { path: N_PARTS_PICK_PAGE_ROUTE, element: }, { path: PARTS_COLLECTION_PAGE_ROUTE, element: }, + { path: SHARE_PAGE_ROUTE, element: }, { path: N_QUIZ_EVENT_PAGE_WINNER_APLLY_PAGE_ROUTE, element: ,