diff --git a/app/app/page.tsx b/app/app/page.tsx new file mode 100644 index 00000000..cb36bc8e --- /dev/null +++ b/app/app/page.tsx @@ -0,0 +1,5 @@ +import { LandingPage } from "#/pageComponents/landing"; + +export default function AppHomePage() { + return ; +} diff --git a/app/gathering/create/page.tsx b/app/gathering/create/page.tsx index 40101c30..3753d960 100644 --- a/app/gathering/create/page.tsx +++ b/app/gathering/create/page.tsx @@ -28,7 +28,7 @@ export default function GatheringCreatePage() { const handleBackward = () => { if (isFirstStep) { - router.push("/"); + router.push("/app"); } else { back(); } diff --git a/app/page.tsx b/app/page.tsx index 0909840d..280e82e8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,5 @@ -import { LandingPage } from "#/pageComponents/landing"; +import { ServiceLandingPage } from "#/pageComponents/serviceLanding"; export default function Home() { - return ; + return ; } diff --git a/public/images/service-landing/feature-1-screen-a.png b/public/images/service-landing/feature-1-screen-a.png new file mode 100644 index 00000000..a046f10a Binary files /dev/null and b/public/images/service-landing/feature-1-screen-a.png differ diff --git a/public/images/service-landing/feature-1-screen-b.png b/public/images/service-landing/feature-1-screen-b.png new file mode 100644 index 00000000..073d0c15 Binary files /dev/null and b/public/images/service-landing/feature-1-screen-b.png differ diff --git a/public/images/service-landing/feature-2-screen-a.png b/public/images/service-landing/feature-2-screen-a.png new file mode 100644 index 00000000..bd59fefa Binary files /dev/null and b/public/images/service-landing/feature-2-screen-a.png differ diff --git a/public/images/service-landing/feature-2-screen-b.png b/public/images/service-landing/feature-2-screen-b.png new file mode 100644 index 00000000..8c8916e0 Binary files /dev/null and b/public/images/service-landing/feature-2-screen-b.png differ diff --git a/public/images/service-landing/feature-3-screen-a.png b/public/images/service-landing/feature-3-screen-a.png new file mode 100644 index 00000000..3d7daf44 Binary files /dev/null and b/public/images/service-landing/feature-3-screen-a.png differ diff --git a/public/images/service-landing/feature-3-screen-b.png b/public/images/service-landing/feature-3-screen-b.png new file mode 100644 index 00000000..355a65fb Binary files /dev/null and b/public/images/service-landing/feature-3-screen-b.png differ diff --git a/public/images/service-landing/feature-3-screen-c.png b/public/images/service-landing/feature-3-screen-c.png new file mode 100644 index 00000000..63a501d9 Binary files /dev/null and b/public/images/service-landing/feature-3-screen-c.png differ diff --git a/public/images/service-landing/feature-4-screen.png b/public/images/service-landing/feature-4-screen.png new file mode 100644 index 00000000..ed2da81c Binary files /dev/null and b/public/images/service-landing/feature-4-screen.png differ diff --git a/public/images/service-landing/feature-4-tooltip.png b/public/images/service-landing/feature-4-tooltip.png new file mode 100644 index 00000000..441db00f Binary files /dev/null and b/public/images/service-landing/feature-4-tooltip.png differ diff --git a/public/images/service-landing/feature-5-og-card.png b/public/images/service-landing/feature-5-og-card.png new file mode 100644 index 00000000..75838fa4 Binary files /dev/null and b/public/images/service-landing/feature-5-og-card.png differ diff --git a/src/constants/gathering/opinion/index.ts b/src/constants/gathering/opinion/index.ts index 79358d47..e100e399 100644 --- a/src/constants/gathering/opinion/index.ts +++ b/src/constants/gathering/opinion/index.ts @@ -1,14 +1,13 @@ export { CATEGORY, + type Category, CATEGORY_LABEL, CATEGORY_LIST, CATEGORY_VALUES, - type Category, } from "./category"; export { DISTANCE_OPTIONS, DISTANCE_RANGE, - DISTANCE_RANGE_LABEL, DISTANCE_RANGE_WALKING_MINUTES, type DistanceRange, } from "./distance"; @@ -16,6 +15,6 @@ export { OPINION_STEP_ORDER, OPINION_TOTAL_STEPS } from "./funnel"; export { MOCK_MEETING_DATA } from "./meeting"; export { RANK, RANK_LABEL, RANK_LIST, type RankKey } from "./rank"; export { RecommendationResultStatus } from "./recommendationResultStatus"; -export { REGION, REGION_LABEL, REGION_OPTIONS, type Region } from "./region"; +export { REGION, type Region, REGION_LABEL, REGION_OPTIONS } from "./region"; export { TIME_SLOT_LABEL } from "./timeSlot"; export { UI_TEXT } from "./ui-text"; diff --git a/src/hooks/gathering/useOpinionForm.tsx b/src/hooks/gathering/useOpinionForm.tsx index ad1df40f..681d8451 100644 --- a/src/hooks/gathering/useOpinionForm.tsx +++ b/src/hooks/gathering/useOpinionForm.tsx @@ -19,7 +19,8 @@ export function useOpinionForm() { const router = useRouter(); const { accessKey } = useParams<{ accessKey: string }>(); - const { refetch: refetchRecommendResult } = useGetRecommendResult(accessKey); + const { refetch: refetchRecommendResult } = + useGetRecommendResult(accessKey); const { mutateAsync: createParticipant, isPending } = useCreateParticipant(); @@ -41,7 +42,7 @@ export function useOpinionForm() { const handleClickShowResultButton = async () => { await refetchRecommendResult(); router.push(`/gathering/${accessKey}/opinion/result`); - } + }; const handleSubmit = methods.handleSubmit(async (data) => { try { diff --git a/src/pageComponents/gathering/opinion/form/ToastLinkButton.tsx b/src/pageComponents/gathering/opinion/form/ToastLinkButton.tsx index 58d09199..99d7da6d 100644 --- a/src/pageComponents/gathering/opinion/form/ToastLinkButton.tsx +++ b/src/pageComponents/gathering/opinion/form/ToastLinkButton.tsx @@ -16,7 +16,7 @@ export const ToastLinkButton = ({ label, onClick }: ToastLinkButtonProps) => { className={twJoin( "ygi:ml-auto ygi:flex ygi:items-center ygi:justify-center ygi:gap-0.5", "ygi:cursor-pointer ygi:body-14-sb ygi:text-palette-primary-500", - "ygi:text-nowrap" + "ygi:text-nowrap", )} onClick={onClick} > diff --git a/src/pageComponents/serviceLanding/CtaSection.tsx b/src/pageComponents/serviceLanding/CtaSection.tsx new file mode 100644 index 00000000..eddce080 --- /dev/null +++ b/src/pageComponents/serviceLanding/CtaSection.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { motion } from "motion/react"; +import Link from "next/link"; + +import { useScrollReveal } from "./useScrollReveal"; + +export const CtaSection = () => { + const { ref, isInView } = useScrollReveal(); + + return ( + + + + {"지금 바로 "} + 요기잇 + {"으로\n맛집을 추천 받아요"} + + + 바로 시작하기 + + + + ); +}; diff --git a/src/pageComponents/serviceLanding/Feature1Section.tsx b/src/pageComponents/serviceLanding/Feature1Section.tsx new file mode 100644 index 00000000..497c0a08 --- /dev/null +++ b/src/pageComponents/serviceLanding/Feature1Section.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { motion } from "motion/react"; +import Image from "next/image"; + +import { FeatureText } from "./FeatureText"; +import { useScrollReveal } from "./useScrollReveal"; + +export const Feature1Section = () => { + const { ref, isInView } = useScrollReveal(); + + return ( + + + + {/* overflow-hidden으로 하단 overflow 초기 상태 클리핑 */} + + + {( + [ + "feature-1-screen-a", + "feature-1-screen-b", + ] as const + ).map((name) => ( + + ))} + + + + + ); +}; diff --git a/src/pageComponents/serviceLanding/Feature2Section.tsx b/src/pageComponents/serviceLanding/Feature2Section.tsx new file mode 100644 index 00000000..b3ddd787 --- /dev/null +++ b/src/pageComponents/serviceLanding/Feature2Section.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { motion } from "motion/react"; +import Image from "next/image"; + +import { FeatureText } from "./FeatureText"; +import { useScrollReveal } from "./useScrollReveal"; + +export const Feature2Section = () => { + const { ref, isInView } = useScrollReveal(); + + return ( + + + + {"나의 "} + + 의견 입력 + + {" 한번에"} + > + } + isDark={false} + isInView={isInView} + /> + {/* overflow-hidden으로 하단 overflow 초기 상태 클리핑 */} + + + {( + [ + "feature-2-screen-a", + "feature-2-screen-b", + ] as const + ).map((name) => ( + + ))} + + + + + ); +}; diff --git a/src/pageComponents/serviceLanding/Feature3Section.tsx b/src/pageComponents/serviceLanding/Feature3Section.tsx new file mode 100644 index 00000000..18f5de64 --- /dev/null +++ b/src/pageComponents/serviceLanding/Feature3Section.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { motion } from "motion/react"; +import Image from "next/image"; + +import { FeatureText } from "./FeatureText"; +import { useScrollReveal } from "./useScrollReveal"; + +const feature3Screens = [ + { name: "feature-3-screen-a", direction: -1, height: 151 }, + { name: "feature-3-screen-b", direction: 1, height: 146 }, + { name: "feature-3-screen-c", direction: -1, height: 139 }, +]; + +export const Feature3Section = () => { + const { ref, isInView } = useScrollReveal(); + + return ( + + + + + {feature3Screens.map( + ({ name, direction, height }, index) => ( + + + + ), + )} + + + + ); +}; diff --git a/src/pageComponents/serviceLanding/Feature4Section.tsx b/src/pageComponents/serviceLanding/Feature4Section.tsx new file mode 100644 index 00000000..476567d6 --- /dev/null +++ b/src/pageComponents/serviceLanding/Feature4Section.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { motion } from "motion/react"; +import Image from "next/image"; + +import { FeatureText } from "./FeatureText"; +import { useScrollReveal } from "./useScrollReveal"; + +export const Feature4Section = () => { + const { ref, isInView } = useScrollReveal(); + + return ( + + + + + {/* 투표 필터 요약 카드 - 아래에서 위로 fade + scale */} + + + + {/* 맛집 추천 리스트 - bottom overflow + scale 등장 */} + + + + + + + + + ); +}; diff --git a/src/pageComponents/serviceLanding/Feature5Section.tsx b/src/pageComponents/serviceLanding/Feature5Section.tsx new file mode 100644 index 00000000..49d46f0a --- /dev/null +++ b/src/pageComponents/serviceLanding/Feature5Section.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { motion } from "motion/react"; +import Image from "next/image"; + +import { FeatureText } from "./FeatureText"; +import { SpeechBubble } from "./SpeechBubble"; +import { useScrollReveal } from "./useScrollReveal"; + +export const Feature5Section = () => { + const { ref, isInView } = useScrollReveal(); + + return ( + + + + + {/* OG 카드 + 말풍선 컨테이너 - 각 요소 개별 순차 애니메이션 */} + + {/* ① 오렌지 말풍선 - 좌→우 (delay 0.1s) */} + + + + + {/* ② OG 링크 미리보기 카드 - 좌→우 (delay 0.26s) */} + + + + + {/* ③ 흰 말풍선 1 - 우→좌 (delay 0.44s) */} + + + + + {/* ④ 흰 말풍선 2 - 우→좌 (delay 0.6s) */} + + + + + + + ); +}; diff --git a/src/pageComponents/serviceLanding/FeatureText.tsx b/src/pageComponents/serviceLanding/FeatureText.tsx new file mode 100644 index 00000000..de741d48 --- /dev/null +++ b/src/pageComponents/serviceLanding/FeatureText.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { motion } from "motion/react"; +import { type ReactNode } from "react"; + +interface FeatureTextProps { + caption: string; + headline: ReactNode; + isDark: boolean; + isInView: boolean; +} + +export const FeatureText = ({ + caption, + headline, + isDark, + isInView, +}: FeatureTextProps) => ( + + + {caption} + + + {headline} + + +); diff --git a/src/pageComponents/serviceLanding/HeroSection.tsx b/src/pageComponents/serviceLanding/HeroSection.tsx new file mode 100644 index 00000000..b09eab6f --- /dev/null +++ b/src/pageComponents/serviceLanding/HeroSection.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { motion } from "motion/react"; +import dynamic from "next/dynamic"; +import Image from "next/image"; +import Link from "next/link"; + +import { LandingLogoIcon } from "#/icons/landingLogoIcon"; + +const Player = dynamic( + () => + import("@lottiefiles/react-lottie-player").then( + (module) => module.Player, + ), + { ssr: false }, +); + +export const HeroSection = () => { + return ( + + + {/* Text block */} + + + + + + + + + + + 바로 시작하기 + + + + {/* Characters - Lottie */} + + + + + + + + ); +}; diff --git a/src/pageComponents/serviceLanding/LandingFooter.tsx b/src/pageComponents/serviceLanding/LandingFooter.tsx new file mode 100644 index 00000000..ca75cacb --- /dev/null +++ b/src/pageComponents/serviceLanding/LandingFooter.tsx @@ -0,0 +1,35 @@ +export const LandingFooter = () => ( + +); diff --git a/src/pageComponents/serviceLanding/Navbar.tsx b/src/pageComponents/serviceLanding/Navbar.tsx new file mode 100644 index 00000000..2398a063 --- /dev/null +++ b/src/pageComponents/serviceLanding/Navbar.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { motion } from "motion/react"; +import Link from "next/link"; +import { useEffect, useState } from "react"; + +import { LandingLogoIcon } from "#/icons/landingLogoIcon"; + +export const Navbar = () => { + const [visible, setVisible] = useState(false); + + useEffect(() => { + const sentinel = document.getElementById("hero-sentinel"); + const onScroll = () => { + if (sentinel) { + setVisible(sentinel.getBoundingClientRect().top < 0); + } + }; + window.addEventListener("scroll", onScroll, { passive: true }); + return () => window.removeEventListener("scroll", onScroll); + }, []); + + return ( + + + + + 바로 시작하기 + + + + ); +}; diff --git a/src/pageComponents/serviceLanding/PainPointSection.tsx b/src/pageComponents/serviceLanding/PainPointSection.tsx new file mode 100644 index 00000000..1c673c7f --- /dev/null +++ b/src/pageComponents/serviceLanding/PainPointSection.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { motion, useInView } from "motion/react"; +import { useRef } from "react"; + +import { MeetingCompleteIllustration } from "#/components/illustrations/MeetingCompleteIllustration"; + +export const PainPointSection = () => { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.2 }); + + return ( + + + {"밥약속 생길 때마다 했던 고민\n\u201c어디가지..?\u201d"} + + + + + + + 이제 5분이면 끝! + + + + ); +}; diff --git a/src/pageComponents/serviceLanding/ServiceLandingPage.tsx b/src/pageComponents/serviceLanding/ServiceLandingPage.tsx new file mode 100644 index 00000000..b3944891 --- /dev/null +++ b/src/pageComponents/serviceLanding/ServiceLandingPage.tsx @@ -0,0 +1,30 @@ +import { CtaSection } from "./CtaSection"; +import { Feature1Section } from "./Feature1Section"; +import { Feature2Section } from "./Feature2Section"; +import { Feature3Section } from "./Feature3Section"; +import { Feature4Section } from "./Feature4Section"; +import { Feature5Section } from "./Feature5Section"; +import { HeroSection } from "./HeroSection"; +import { LandingFooter } from "./LandingFooter"; +import { Navbar } from "./Navbar"; +import { PainPointSection } from "./PainPointSection"; + +export const ServiceLandingPage = () => { + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/src/pageComponents/serviceLanding/SpeechBubble.tsx b/src/pageComponents/serviceLanding/SpeechBubble.tsx new file mode 100644 index 00000000..6a605287 --- /dev/null +++ b/src/pageComponents/serviceLanding/SpeechBubble.tsx @@ -0,0 +1,58 @@ +import type { SVGProps } from "react"; + +import { colors } from "#/constants/color"; + +const TailLeft = ({ fill }: SVGProps) => ( + + + +); + +const TailRight = ({ fill }: SVGProps) => ( + + + +); + +interface SpeechBubbleProps { + text: string; + direction: "left" | "right"; + variant: "primary" | "white"; +} + +export const SpeechBubble = ({ + text, + direction, + variant, +}: SpeechBubbleProps) => { + const isPrimary = variant === "primary"; + const backgroundColor = isPrimary + ? colors.palette.primary[500] + : colors.palette.common.white; + + return ( + + {direction === "left" && } + + {text} + + {direction === "right" && } + + ); +}; diff --git a/src/pageComponents/serviceLanding/index.ts b/src/pageComponents/serviceLanding/index.ts new file mode 100644 index 00000000..9f8b5691 --- /dev/null +++ b/src/pageComponents/serviceLanding/index.ts @@ -0,0 +1 @@ +export { ServiceLandingPage } from "./ServiceLandingPage"; diff --git a/src/pageComponents/serviceLanding/useScrollReveal.ts b/src/pageComponents/serviceLanding/useScrollReveal.ts new file mode 100644 index 00000000..43aaafdb --- /dev/null +++ b/src/pageComponents/serviceLanding/useScrollReveal.ts @@ -0,0 +1,8 @@ +import { useInView } from "motion/react"; +import { useRef } from "react"; + +export const useScrollReveal = () => { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.2 }); + return { ref, isInView }; +};
+ {"지금 바로 "} + 요기잇 + {"으로\n맛집을 추천 받아요"} +
+ {caption} +
+ {headline} +
+ {"밥약속 생길 때마다 했던 고민\n\u201c어디가지..?\u201d"} +
+ 이제 5분이면 끝! +