From 5504f37767ecb26f7fc2f63663e3ac5fd219bdef Mon Sep 17 00:00:00 2001 From: HyunseokLEE Date: Mon, 17 Nov 2025 21:50:25 +0900 Subject: [PATCH 01/16] =?UTF-8?q?CDP-216=20chore=E2=9A=99=EF=B8=8F=20(fram?= =?UTF-8?q?er-motion):=20framer=20motion=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/package.json b/package.json index 3df975e..0ebdbda 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.23.24", "lucide-react": "^0.544.0", "next": "15.5.3", "next-seo": "^6.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 148530e..d7fced6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + framer-motion: + specifier: ^12.23.24 + version: 12.23.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0) lucide-react: specifier: ^0.544.0 version: 0.544.0(react@19.1.0) @@ -2247,6 +2250,23 @@ packages: } engines: { node: ">= 6" } + framer-motion@12.23.24: + resolution: + { + integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==, + } + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + function-bind@1.1.2: resolution: { @@ -3114,6 +3134,18 @@ packages: engines: { node: ">=10" } hasBin: true + motion-dom@12.23.23: + resolution: + { + integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==, + } + + motion-utils@12.23.6: + resolution: + { + integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==, + } + ms@2.1.3: resolution: { @@ -5657,6 +5689,15 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + framer-motion@12.23.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + motion-dom: 12.23.23 + motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -6114,6 +6155,12 @@ snapshots: mkdirp@3.0.1: {} + motion-dom@12.23.23: + dependencies: + motion-utils: 12.23.6 + + motion-utils@12.23.6: {} + ms@2.1.3: {} nano-spawn@1.0.3: {} From 393d0d10b0057e64e6bc587b54761528c944911c Mon Sep 17 00:00:00 2001 From: HyunseokLEE Date: Tue, 18 Nov 2025 00:46:16 +0900 Subject: [PATCH 02/16] =?UTF-8?q?CDP-216=20feat=E2=9C=A8=20(landing):=20la?= =?UTF-8?q?nding=20motion=20=EB=AA=A8=EC=95=84=EB=86=93=EC=9D=80=20ts=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/variants/motion.landing.ts | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/lib/variants/motion.landing.ts diff --git a/src/lib/variants/motion.landing.ts b/src/lib/variants/motion.landing.ts new file mode 100644 index 0000000..15b4a09 --- /dev/null +++ b/src/lib/variants/motion.landing.ts @@ -0,0 +1,69 @@ +import type { MotionProps, TargetAndTransition, Transition, Variants } from "framer-motion"; + +/** 뷰포트 트리거 공통 설정 */ +export const viewportOnce30: MotionProps["viewport"] = { + once: true, + amount: 0.3, +}; + +export const viewportOnce35: MotionProps["viewport"] = { + once: true, + amount: 0.35, +}; + +export const viewportOnce25: MotionProps["viewport"] = { + once: true, + amount: 0.25, +}; + +/** 섹션 기본 등장 트랜지션 */ +export const sectionTransition: Transition = { + duration: 0.7, + ease: "easeOut", +}; + +export const headerTransition: Transition = { + duration: 0.6, + ease: "easeOut", +}; + +/** 방향별 페이드 + 슬라이드 */ +export const fadeInFromRight: Variants = { + hidden: { opacity: 0, x: 40 }, + visible: { opacity: 1, x: 0 }, +}; + +export const fadeInFromLeft: Variants = { + hidden: { opacity: 0, x: -40 }, + visible: { opacity: 1, x: 0 }, +}; + +export const fadeInUp: Variants = { + hidden: { opacity: 0, y: 24 }, + visible: { opacity: 1, y: 0 }, +}; + +/** Special 섹션 전용 Grid / Item */ +export const specialGridVariants: Variants = { + hidden: {}, + visible: { + transition: { staggerChildren: 0.18 }, + }, +}; + +export const specialCardVariants: Variants = { + hidden: { opacity: 0, y: 18 }, + visible: { opacity: 1, y: 0 }, +}; + +/** Hero CTA: 통통 튀는 모션만 사용 */ +export const heroCtaBounceAnimate: TargetAndTransition = { + y: [0, -10, 0, -4, 0], +}; + +export const heroCtaBounceTransition: Transition = { + duration: 0.45, + repeat: Infinity, + repeatDelay: 0.7, + ease: "easeInOut", +}; From 5084dc6850125289c2bc1e1a1f083878cb019686 Mon Sep 17 00:00:00 2001 From: HyunseokLEE Date: Tue, 18 Nov 2025 00:46:52 +0900 Subject: [PATCH 03/16] =?UTF-8?q?CDP-216=20refactor=F0=9F=94=A8=20(landing?= =?UTF-8?q?):=20framer=20motion=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../landing/LandingFeatureSection2.tsx | 18 ++++---- .../landing/LandingFeaturesSection1.tsx | 18 ++++---- src/components/landing/LandingHeroCtas.tsx | 27 ++++++------ .../landing/LandingSpecialFeatureGrid.tsx | 41 ++++++++----------- .../landing/LandingSpecialFeaturesSection.tsx | 25 ++++++----- src/hooks/useInView.ts | 36 ---------------- 6 files changed, 63 insertions(+), 102 deletions(-) delete mode 100644 src/hooks/useInView.ts diff --git a/src/components/landing/LandingFeatureSection2.tsx b/src/components/landing/LandingFeatureSection2.tsx index 47f56ee..9cca848 100644 --- a/src/components/landing/LandingFeatureSection2.tsx +++ b/src/components/landing/LandingFeatureSection2.tsx @@ -2,30 +2,30 @@ import { LandingFeatureText } from "@/components/landing/LandingFeatureText"; import { LandingLayoutPreview } from "@/components/landing/LandingLayoutPreview"; -import { useInView } from "@/hooks/useInView"; import { cn } from "@/lib/utils"; +import { fadeInFromLeft, sectionTransition, viewportOnce35 } from "@/lib/variants/motion.landing"; import type { LandingFeaturesSection2Props } from "@/types/landing"; +import { motion } from "framer-motion"; export function LandingFeaturesSection2({ className }: LandingFeaturesSection2Props) { - const { ref, isInView } = useInView({ - threshold: 0.5, - once: false, - }); return ( -
-
+ ); } diff --git a/src/components/landing/LandingFeaturesSection1.tsx b/src/components/landing/LandingFeaturesSection1.tsx index 354a8e0..85dbd0f 100644 --- a/src/components/landing/LandingFeaturesSection1.tsx +++ b/src/components/landing/LandingFeaturesSection1.tsx @@ -1,31 +1,31 @@ "use client"; import { LandingFeatureGrid } from "@/components/landing/LandingFeatureGrid"; -import { useInView } from "@/hooks/useInView"; import { cn } from "@/lib/utils"; +import { fadeInFromRight, sectionTransition, viewportOnce30 } from "@/lib/variants/motion.landing"; import { useFeaturePreviewStore } from "@/stores/featurePreviewStore"; import type { LandingFeaturesSection1Props } from "@/types/landing"; +import { motion } from "framer-motion"; import Image from "next/image"; export function LandingFeaturesSection1({ className }: LandingFeaturesSection1Props) { const activeFeature = useFeaturePreviewStore((state) => state.activeFeature); - const { ref, isInView } = useInView({ - threshold: 0.5, - once: false, - }); return ( -
{/* Left: 제목 + 기능 리스트 */}
@@ -60,6 +60,6 @@ export function LandingFeaturesSection1({ className }: LandingFeaturesSection1Pr
-
+ ); } diff --git a/src/components/landing/LandingHeroCtas.tsx b/src/components/landing/LandingHeroCtas.tsx index 28c23ea..f481382 100644 --- a/src/components/landing/LandingHeroCtas.tsx +++ b/src/components/landing/LandingHeroCtas.tsx @@ -1,23 +1,26 @@ +"use client"; + import { cn } from "@/lib/utils"; +import { heroCtaBounceAnimate, heroCtaBounceTransition } from "@/lib/variants/motion.landing"; import { Button } from "@/shared/button"; import { Icon } from "@/shared/Icon"; +import { motion } from "framer-motion"; import Link from "next/link"; export function LandingHeroCtas() { return (
- - - + + + + + +
- + ); } diff --git a/src/components/auth/signup/SignupGroupButton.tsx b/src/components/auth/signup/SignupGroupButton.tsx index 02fffd5..e0d5031 100644 --- a/src/components/auth/signup/SignupGroupButton.tsx +++ b/src/components/auth/signup/SignupGroupButton.tsx @@ -1,25 +1,24 @@ "use client"; -import { useFadeSlideInOnMount } from "@/hooks/useMotionPresets"; import { cn } from "@/lib/utils"; +import { authFadeSlideUp, authTransition } from "@/lib/variants/motion.auth"; import { Button } from "@/shared/button"; import { Icon } from "@/shared/Icon"; +import { SignupGroupButtonProps } from "@/types/auth"; +import { motion } from "framer-motion"; import Image from "next/image"; import Link from "next/link"; import * as React from "react"; -interface SignupGroupButtonProps { - className?: string; -} - export const SignupGroupButton = React.forwardRef( - ({ className }, ref) => { - const fadeClass = useFadeSlideInOnMount("up"); - + ({ className }) => { return ( -
{/* 1) 일반 회원가입 → 이메일 회원가입 폼 페이지로 이동 */} @@ -76,7 +75,7 @@ export const SignupGroupButton = React.forwardRef카카오로 가입 -
+ ); }, ); diff --git a/src/hooks/useMotionPresets.ts b/src/hooks/useMotionPresets.ts deleted file mode 100644 index fa3f6fc..0000000 --- a/src/hooks/useMotionPresets.ts +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import type { FadeSlideOptions, MotionDirection } from "@/types/motion"; -import { useEffect, useState } from "react"; - -export function useFadeSlideInOnMount( - direction: MotionDirection = "up", - options: FadeSlideOptions = {}, -): string { - const { distanceClass, durationClass } = options; - const [isVisible, setIsVisible] = useState(false); - - useEffect(() => { - const timer = window.setTimeout(() => { - setIsVisible(true); - }, 0); - - return () => window.clearTimeout(timer); - }, []); - - const baseTransition = durationClass ?? "transition-all duration-1000 ease-out"; - - const hiddenByDirection: Record = { - up: distanceClass ? `opacity-0 ${distanceClass}` : "opacity-0 translate-y-4", - down: distanceClass ? `opacity-0 ${distanceClass}` : "opacity-0 -translate-y-4", - left: distanceClass ? `opacity-0 ${distanceClass}` : "opacity-0 translate-x-4", - right: distanceClass ? `opacity-0 ${distanceClass}` : "opacity-0 -translate-x-4", - }; - - const visibleClass = "opacity-100 translate-x-0 translate-y-0"; - - const stateClass = isVisible ? visibleClass : hiddenByDirection[direction]; - - return `${baseTransition} ${stateClass}`; -} - -export function useStepSlideOnMount( - from: "left" | "right" = "right", - options: FadeSlideOptions = {}, -): string { - const { distanceClass, durationClass } = options; - const [isVisible, setIsVisible] = useState(false); - - useEffect(() => { - const timer = window.setTimeout(() => { - setIsVisible(true); - }, 0); - - return () => window.clearTimeout(timer); - }, []); - - const baseTransition = durationClass ?? "transition-all duration-300 ease-out"; - - const hidden = - from === "right" ? (distanceClass ?? "translate-x-4") : (distanceClass ?? "-translate-x-4"); - - const hiddenClass = `opacity-0 ${hidden}`; - const visibleClass = "opacity-100 translate-x-0"; - - const stateClass = isVisible ? visibleClass : hiddenClass; - - return `${baseTransition} ${stateClass}`; -} diff --git a/src/lib/variants/motion.auth.ts b/src/lib/variants/motion.auth.ts new file mode 100644 index 0000000..ac1ad4b --- /dev/null +++ b/src/lib/variants/motion.auth.ts @@ -0,0 +1,38 @@ +import type { Transition, Variants } from "framer-motion"; + +/** + * 단일 폼용: 아래 → 위 슬라이드 + 페이드 인 + */ +export const authFadeSlideUp: Variants = { + hidden: { + opacity: 0, + y: 24, + }, + visible: { + opacity: 1, + y: 0, + }, +}; + +/** + * 다단계 폼용: 좌우 슬라이드 인/아웃 + */ +export const authStepSlide: Variants = { + enter: (direction: "forward" | "backward") => ({ + opacity: 0, + x: direction === "forward" ? 40 : -40, + }), + center: { + opacity: 1, + x: 0, + }, + exit: (direction: "forward" | "backward") => ({ + opacity: 0, + x: direction === "forward" ? -40 : 40, + }), +}; + +export const authTransition: Transition = { + duration: 0.6, + ease: "easeOut", +}; diff --git a/src/types/inView.ts b/src/types/inView.ts deleted file mode 100644 index 2462cbd..0000000 --- a/src/types/inView.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { RefObject } from "react"; - -export interface UseInViewOptions { - threshold?: number; - rootMargin?: string; - once?: boolean; -} - -export interface UseInViewResult { - ref: RefObject; - isInView: boolean; -} diff --git a/src/types/motion.ts b/src/types/motion.ts deleted file mode 100644 index 690ad7e..0000000 --- a/src/types/motion.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type MotionDirection = "up" | "down" | "left" | "right"; - -/** Tailwind 기반 페이드+슬라이드 옵션 */ -export interface FadeSlideOptions { - distanceClass?: string; - - durationClass?: string; -} From aabbd6e33326d27d46e28fd0ac36f7885ae0b815 Mon Sep 17 00:00:00 2001 From: HyunseokLEE Date: Wed, 19 Nov 2025 00:30:34 +0900 Subject: [PATCH 05/16] =?UTF-8?q?CDP-216=20feat=E2=9C=A8=20(signup):=20sig?= =?UTF-8?q?nup=20form=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/signup/StepTransitionWrapper.tsx | 28 +++++++++++++++ src/types/auth.ts | 35 ++++++++++++------- 2 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 src/components/auth/signup/StepTransitionWrapper.tsx diff --git a/src/components/auth/signup/StepTransitionWrapper.tsx b/src/components/auth/signup/StepTransitionWrapper.tsx new file mode 100644 index 0000000..d7bf117 --- /dev/null +++ b/src/components/auth/signup/StepTransitionWrapper.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { authStepSlide, authTransition } from "@/lib/variants/motion.auth"; +import { StepTransitionWrapperProps } from "@/types/auth"; +import { AnimatePresence, motion } from "framer-motion"; + +export function StepTransitionWrapper({ + stepKey, + direction, + children, +}: StepTransitionWrapperProps) { + return ( + + + {children} + + + ); +} diff --git a/src/types/auth.ts b/src/types/auth.ts index 4d561c2..8dd0575 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -22,24 +22,33 @@ export interface AuthFormState { validateLogin: () => boolean; } +export interface SignupGroupButtonProps { + className?: string; +} + +/** 각 스텝 인풋에 공통으로 필요한 최소 정보 */ +export interface SignupFieldBaseProps { + fieldId: string; + fieldName: string; +} + +/** 회원가입 스텝 */ +export type SignupStepKey = "email" | "name" | "password" | "terms"; + +export interface StepTransitionWrapperProps { + stepKey: string; + direction: "forward" | "backward"; + children: React.ReactNode; +} + +// 스텝 이동 방향 +export type StepDirection = "forward" | "backward"; + export interface SignupFormProps { className?: string; - /** 인풋 id / name */ fieldId: string; fieldName: string; - /** 라벨 텍스트 (예: 이름, 이메일) */ - label: string; - /** 인풋 타입 */ - type?: "text" | "email" | "password"; - /** placeholder */ - placeholder?: string; - /** autoComplete 힌트 */ - autoComplete?: string; - - /** 임시: 다음 스텝으로 이동할 링크 */ nextHref: string; - - /** 임시: 이전 스텝으로 이동할 링크 (있을 때만 버튼 노출) */ prevHref?: string; } From edc9b216dd264e635daafeebcfaeba1b626ab509 Mon Sep 17 00:00:00 2001 From: HyunseokLEE Date: Wed, 19 Nov 2025 02:14:34 +0900 Subject: [PATCH 06/16] =?UTF-8?q?CDP-217=20feat=E2=9C=A8=20(signup):=20?= =?UTF-8?q?=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(auth)/signup/email/page.tsx | 4 - src/app/(auth)/signup/name/page.tsx | 9 +- src/app/(auth)/signup/password/page.tsx | 4 - src/app/(auth)/signup/terms/page.tsx | 4 - .../auth/signup/SignupEmailStep.tsx | 22 +++ src/components/auth/signup/SignupForm.tsx | 135 ++++++++++-------- src/components/auth/signup/SignupNameStep.tsx | 22 +++ .../auth/signup/SignupPasswordStep.tsx | 41 ++++++ .../auth/signup/SignupTermsStep.tsx | 45 ++++++ src/lib/constants.ts | 8 ++ src/stores/signupStepStore.ts | 45 ++++++ src/types/auth.ts | 20 ++- 12 files changed, 272 insertions(+), 87 deletions(-) create mode 100644 src/components/auth/signup/SignupEmailStep.tsx create mode 100644 src/components/auth/signup/SignupNameStep.tsx create mode 100644 src/components/auth/signup/SignupPasswordStep.tsx create mode 100644 src/components/auth/signup/SignupTermsStep.tsx create mode 100644 src/stores/signupStepStore.ts diff --git a/src/app/(auth)/signup/email/page.tsx b/src/app/(auth)/signup/email/page.tsx index 3c13f44..88f4d46 100644 --- a/src/app/(auth)/signup/email/page.tsx +++ b/src/app/(auth)/signup/email/page.tsx @@ -25,10 +25,6 @@ export default function SignupEmailPage() { diff --git a/src/app/(auth)/signup/name/page.tsx b/src/app/(auth)/signup/name/page.tsx index b74a13f..703830b 100644 --- a/src/app/(auth)/signup/name/page.tsx +++ b/src/app/(auth)/signup/name/page.tsx @@ -22,14 +22,7 @@ export default function SignupNamePage() { - + ); diff --git a/src/app/(auth)/signup/password/page.tsx b/src/app/(auth)/signup/password/page.tsx index 683491e..b776666 100644 --- a/src/app/(auth)/signup/password/page.tsx +++ b/src/app/(auth)/signup/password/page.tsx @@ -25,10 +25,6 @@ export default function SignupPasswordPage() { diff --git a/src/app/(auth)/signup/terms/page.tsx b/src/app/(auth)/signup/terms/page.tsx index 0034491..b301bd9 100644 --- a/src/app/(auth)/signup/terms/page.tsx +++ b/src/app/(auth)/signup/terms/page.tsx @@ -25,10 +25,6 @@ export default function SignupTermsPage() { diff --git a/src/components/auth/signup/SignupEmailStep.tsx b/src/components/auth/signup/SignupEmailStep.tsx new file mode 100644 index 0000000..0da0b35 --- /dev/null +++ b/src/components/auth/signup/SignupEmailStep.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { Input } from "@/shared/input"; +import type { SignupFieldBaseProps } from "@/types/auth"; + +export function SignupEmailStep({ fieldId, fieldName }: SignupFieldBaseProps) { + return ( +
+ + +
+ ); +} diff --git a/src/components/auth/signup/SignupForm.tsx b/src/components/auth/signup/SignupForm.tsx index 6585e50..f8ec04a 100644 --- a/src/components/auth/signup/SignupForm.tsx +++ b/src/components/auth/signup/SignupForm.tsx @@ -1,76 +1,89 @@ "use client"; +import { SIGNUP_STEP_ORDER } from "@/lib/constants"; import { cn } from "@/lib/utils"; import { Button } from "@/shared/button"; -import { Input } from "@/shared/input"; -import { SignupFormProps } from "@/types/auth"; +import { useSignupStepStore } from "@/stores/signupStepStore"; +import type { SignupFormProps } from "@/types/auth"; import Link from "next/link"; +import { SignupEmailStep } from "./SignupEmailStep"; +import { SignupNameStep } from "./SignupNameStep"; +import { SignupPasswordStep } from "./SignupPasswordStep"; import { SignupStepIndicator } from "./SignupStepIndicator"; +import { SignupTermsStep } from "./SignupTermsStep"; +import { StepTransitionWrapper } from "./StepTransitionWrapper"; + +export function SignupForm({ className, fieldId, fieldName }: SignupFormProps) { + const { step, direction, goNext, goPrev } = useSignupStepStore(); + + const isEmailStep = step === "email"; + const isNameStep = step === "name"; + const isPasswordStep = step === "password"; + const isTermsStep = step === "terms"; + + const currentStepIndex = SIGNUP_STEP_ORDER.indexOf(step); + const currentStepNumber = currentStepIndex + 1; -export function SignupForm({ - className, - fieldId, - fieldName, - label, - type = "text", - placeholder, - autoComplete, - nextHref, - prevHref, -}: SignupFormProps) { return ( -
- - {/* 단일 필드 */} -
- - -
+ + + + + {/* 메인 필드 영역 */} +
+ {isNameStep && } + {isEmailStep && } + {isPasswordStep && } + {isTermsStep && } +
- {/* 다음 / 이전 버튼 (임시: Link로 라우팅) */} -
- - - + {/* 다음 / 이전 버튼 */} +
+ {!isTermsStep && ( + + )} - {prevHref && ( - - + )} + + {!isEmailStep && ( + - - )} -
+ )} +
- {/* 하단 로그인 링크 */} -

- 이미 회원이신가요?{" "} - - 로그인하기 - -

- + {/* 하단 로그인 링크 */} +

+ 이미 회원이신가요?{" "} + + 로그인하기 + +

+ +
); } diff --git a/src/components/auth/signup/SignupNameStep.tsx b/src/components/auth/signup/SignupNameStep.tsx new file mode 100644 index 0000000..2cc0b1d --- /dev/null +++ b/src/components/auth/signup/SignupNameStep.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { Input } from "@/shared/input"; +import type { SignupFieldBaseProps } from "@/types/auth"; + +export function SignupNameStep({ fieldId, fieldName }: SignupFieldBaseProps) { + return ( +
+ + +
+ ); +} diff --git a/src/components/auth/signup/SignupPasswordStep.tsx b/src/components/auth/signup/SignupPasswordStep.tsx new file mode 100644 index 0000000..6a10c14 --- /dev/null +++ b/src/components/auth/signup/SignupPasswordStep.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { Input } from "@/shared/input"; +import type { SignupFieldBaseProps } from "@/types/auth"; + +export function SignupPasswordStep({ fieldId, fieldName }: SignupFieldBaseProps) { + return ( + <> +
+ + +
+ +
+ + +
+ + ); +} diff --git a/src/components/auth/signup/SignupTermsStep.tsx b/src/components/auth/signup/SignupTermsStep.tsx new file mode 100644 index 0000000..8d0bfa7 --- /dev/null +++ b/src/components/auth/signup/SignupTermsStep.tsx @@ -0,0 +1,45 @@ +"use client"; + +export function SignupTermsStep() { + return ( +
+ 약관 동의 + + + + +
+ ); +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 15e7bba..0fae191 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -111,3 +111,11 @@ export const SPECIAL_FEATURES: SpecialFeatureItem[] = [ iconName: "bellRing", }, ]; + +/* ---------------------------------------------- + 🧭 회원가입 단계 — 순서 정의 (Zustand + Router 공용) + ---------------------------------------------- */ + +import type { SignupStepKey } from "@/types/auth"; + +export const SIGNUP_STEP_ORDER: SignupStepKey[] = ["email", "name", "password", "terms"]; diff --git a/src/stores/signupStepStore.ts b/src/stores/signupStepStore.ts new file mode 100644 index 0000000..73dc468 --- /dev/null +++ b/src/stores/signupStepStore.ts @@ -0,0 +1,45 @@ +"use client"; + +import { SIGNUP_STEP_ORDER } from "@/lib/constants"; +import type { StepDirection } from "@/types/auth"; +import { SignupStepState } from "@/types/auth"; +import { create } from "zustand"; + +export const useSignupStepStore = create((set, get) => ({ + step: "email", + direction: "forward", + + goTo: (next) => { + const current = get().step; + if (current === next) return; + + const currentIndex = SIGNUP_STEP_ORDER.indexOf(current); + const nextIndex = SIGNUP_STEP_ORDER.indexOf(next); + + const direction: StepDirection = nextIndex > currentIndex ? "forward" : "backward"; + + set({ step: next, direction }); + }, + + goNext: () => { + const current = get().step; + const currentIndex = SIGNUP_STEP_ORDER.indexOf(current); + const nextIndex = Math.min(currentIndex + 1, SIGNUP_STEP_ORDER.length - 1); + const next = SIGNUP_STEP_ORDER[nextIndex]; + + if (next !== current) { + set({ step: next, direction: "forward" }); + } + }, + + goPrev: () => { + const current = get().step; + const currentIndex = SIGNUP_STEP_ORDER.indexOf(current); + const prevIndex = Math.max(currentIndex - 1, 0); + const prev = SIGNUP_STEP_ORDER[prevIndex]; + + if (prev !== current) { + set({ step: prev, direction: "backward" }); + } + }, +})); diff --git a/src/types/auth.ts b/src/types/auth.ts index 8dd0575..1783f86 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -44,12 +44,12 @@ export interface StepTransitionWrapperProps { // 스텝 이동 방향 export type StepDirection = "forward" | "backward"; -export interface SignupFormProps { - className?: string; - fieldId: string; - fieldName: string; - nextHref: string; - prevHref?: string; +export interface SignupStepState { + step: SignupStepKey; + direction: StepDirection; + goTo: (next: SignupStepKey) => void; + goNext: () => void; + goPrev: () => void; } export interface SignupStepIndicatorProps { @@ -59,3 +59,11 @@ export interface SignupStepIndicatorProps { totalSteps?: number; className?: string; } + +export interface SignupFormProps { + className?: string; + fieldId: string; + fieldName: string; + nextHref: string; + prevHref?: string; +} From f685ba04dcf82217e69224ac7e7b6fe7f7e91db2 Mon Sep 17 00:00:00 2001 From: HyunseokLEE Date: Wed, 19 Nov 2025 16:18:02 +0900 Subject: [PATCH 07/16] =?UTF-8?q?CDP-217=20feat=E2=9C=A8=20(signup-state):?= =?UTF-8?q?=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(auth)/forgot-password/page.tsx | 2 +- src/app/(auth)/forgot-password/reset/page.tsx | 14 --- .../(auth)/forgot-password/verify/page.tsx | 14 --- src/app/(auth)/signup/basic/page.tsx | 24 ++++++ src/app/(auth)/signup/email/page.tsx | 34 -------- src/app/(auth)/signup/name/page.tsx | 29 ------- src/app/(auth)/signup/page.tsx | 2 +- src/app/(auth)/signup/password/page.tsx | 34 -------- src/app/(auth)/signup/terms/page.tsx | 34 -------- src/components/auth/AuthMain.tsx | 15 +++- ...tionWrapper.tsx => AuthStepTransition.tsx} | 10 +-- ...nupStepIndicator.tsx => StepIndicator.tsx} | 8 +- src/components/auth/login/LoginForm.tsx | 86 ++++--------------- .../auth/signup/SignupEmailStep.tsx | 4 +- src/components/auth/signup/SignupForm.tsx | 16 ++-- .../auth/signup/SignupGroupButton.tsx | 2 +- src/components/auth/signup/SignupHeader.tsx | 18 ++++ src/components/auth/signup/SignupNameStep.tsx | 4 +- .../auth/signup/SignupPasswordStep.tsx | 13 ++- src/lib/constants.ts | 43 +++++++++- src/stores/authForm.store.ts | 55 ------------ src/stores/signupStepStore.ts | 7 ++ src/types/auth.ts | 41 +++------ 23 files changed, 156 insertions(+), 353 deletions(-) delete mode 100644 src/app/(auth)/forgot-password/reset/page.tsx delete mode 100644 src/app/(auth)/forgot-password/verify/page.tsx create mode 100644 src/app/(auth)/signup/basic/page.tsx delete mode 100644 src/app/(auth)/signup/email/page.tsx delete mode 100644 src/app/(auth)/signup/name/page.tsx delete mode 100644 src/app/(auth)/signup/password/page.tsx delete mode 100644 src/app/(auth)/signup/terms/page.tsx rename src/components/auth/{signup/StepTransitionWrapper.tsx => AuthStepTransition.tsx} (73%) rename src/components/auth/{signup/SignupStepIndicator.tsx => StepIndicator.tsx} (88%) create mode 100644 src/components/auth/signup/SignupHeader.tsx delete mode 100644 src/stores/authForm.store.ts diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx index 4e12985..6c5f8a6 100644 --- a/src/app/(auth)/forgot-password/page.tsx +++ b/src/app/(auth)/forgot-password/page.tsx @@ -3,7 +3,7 @@ import { makePageMetadata } from "@/seo/metadata"; export const metadata = { ...makePageMetadata({ title: "비밀번호 찾기", - description: "PlanMate 비밀번호 찾기 1단계: 이메일 입력 페이지", + description: "PlanMate 비밀번호 찾기 페이지", canonical: "/forgot-password", }), robots: { index: false, follow: false }, diff --git a/src/app/(auth)/forgot-password/reset/page.tsx b/src/app/(auth)/forgot-password/reset/page.tsx deleted file mode 100644 index 36e36dd..0000000 --- a/src/app/(auth)/forgot-password/reset/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { makePageMetadata } from "@/seo/metadata"; - -export const metadata = { - ...makePageMetadata({ - title: "비밀번호 찾기 — 비밀번호 재설정", - description: "PlanMate 비밀번호 찾기 3단계: 새 비밀번호 설정 페이지", - canonical: "/forgot-password/reset", - }), - robots: { index: false, follow: false }, -}; - -export default function ForgotPasswordResetPage() { - return
비밀번호 찾기 - 비밀번호 재설정 페이지
; -} diff --git a/src/app/(auth)/forgot-password/verify/page.tsx b/src/app/(auth)/forgot-password/verify/page.tsx deleted file mode 100644 index 2784c3a..0000000 --- a/src/app/(auth)/forgot-password/verify/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { makePageMetadata } from "@/seo/metadata"; - -export const metadata = { - ...makePageMetadata({ - title: "비밀번호 찾기 — 인증번호 입력", - description: "PlanMate 비밀번호 찾기 2단계: 이메일 인증번호 입력 페이지", - canonical: "/forgot-password/verify", - }), - robots: { index: false, follow: false }, -}; - -export default function ForgotPasswordVerifyPage() { - return
비밀번호 찾기 - 인증번호 입력 페이지
; -} diff --git a/src/app/(auth)/signup/basic/page.tsx b/src/app/(auth)/signup/basic/page.tsx new file mode 100644 index 0000000..216e494 --- /dev/null +++ b/src/app/(auth)/signup/basic/page.tsx @@ -0,0 +1,24 @@ +import { AuthMain } from "@/components/auth/AuthMain"; +import { SignupForm } from "@/components/auth/signup/SignupForm"; +import { SignupHeader } from "@/components/auth/signup/SignupHeader"; +import { makePageMetadata } from "@/seo/metadata"; + +export const metadata = { + ...makePageMetadata({ + title: "회원가입", + description: "MyPlanMate 회원가입페이지", + canonical: "/signup/basic", + }), + robots: { index: false, follow: false }, +}; + +export default function SignupEmailPage() { + return ( + <> + + + + + + ); +} diff --git a/src/app/(auth)/signup/email/page.tsx b/src/app/(auth)/signup/email/page.tsx deleted file mode 100644 index 88f4d46..0000000 --- a/src/app/(auth)/signup/email/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { AuthHeader } from "@/components/auth/AuthHeader"; -import { AuthMain } from "@/components/auth/AuthMain"; -import { SubTitle, Title } from "@/components/auth/AuthTitle"; -import { SignupForm } from "@/components/auth/signup/SignupForm"; -import { makePageMetadata } from "@/seo/metadata"; - -export const metadata = { - ...makePageMetadata({ - title: "회원가입 — 이메일 입력", - description: "MyPlanMate 회원가입 2단계: 이메일 입력 페이지", - canonical: "/signup/email", - }), - robots: { index: false, follow: false }, -}; - -export default function SignupEmailPage() { - return ( - <> - - 이메일 - 계정으로 사용할 이메일을 알려주세요. - - - - - - - ); -} diff --git a/src/app/(auth)/signup/name/page.tsx b/src/app/(auth)/signup/name/page.tsx deleted file mode 100644 index 703830b..0000000 --- a/src/app/(auth)/signup/name/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { AuthHeader } from "@/components/auth/AuthHeader"; -import { AuthMain } from "@/components/auth/AuthMain"; -import { SubTitle, Title } from "@/components/auth/AuthTitle"; -import { SignupForm } from "@/components/auth/signup/SignupForm"; -import { makePageMetadata } from "@/seo/metadata"; - -export const metadata = { - ...makePageMetadata({ - title: "회원가입 — 이름 입력", - description: "MyPlanMate 회원가입 1단계: 이름 입력 페이지", - canonical: "/signup/name", - }), - robots: { index: false, follow: false }, -}; - -export default function SignupNamePage() { - return ( - <> - - 이름 - 이름을 알려주세요. - - - - - - - ); -} diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 229f6bf..4ed937b 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -7,7 +7,7 @@ import { makePageMetadata } from "@/seo/metadata"; export const metadata = { ...makePageMetadata({ title: "회원가입", - description: "PlanMate 회원가입 페이지", + description: "MyPlanMate 회원가입 페이지", canonical: "/signup", }), robots: { index: false, follow: false }, // 인증 관련 페이지는 검색 제외 diff --git a/src/app/(auth)/signup/password/page.tsx b/src/app/(auth)/signup/password/page.tsx deleted file mode 100644 index b776666..0000000 --- a/src/app/(auth)/signup/password/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { AuthHeader } from "@/components/auth/AuthHeader"; -import { AuthMain } from "@/components/auth/AuthMain"; -import { SubTitle, Title } from "@/components/auth/AuthTitle"; -import { SignupForm } from "@/components/auth/signup/SignupForm"; -import { makePageMetadata } from "@/seo/metadata"; - -export const metadata = { - ...makePageMetadata({ - title: "회원가입 — 비밀번호 설정", - description: "PlanMate 회원가입 3단계: 비밀번호 설정 페이지", - canonical: "/signup/password", - }), - robots: { index: false, follow: false }, -}; - -export default function SignupPasswordPage() { - return ( - <> - - 비밀번호 - 8자 이상 / 특수문자 포함 - - - - - - - ); -} diff --git a/src/app/(auth)/signup/terms/page.tsx b/src/app/(auth)/signup/terms/page.tsx deleted file mode 100644 index b301bd9..0000000 --- a/src/app/(auth)/signup/terms/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { AuthHeader } from "@/components/auth/AuthHeader"; -import { AuthMain } from "@/components/auth/AuthMain"; -import { SubTitle, Title } from "@/components/auth/AuthTitle"; -import { SignupForm } from "@/components/auth/signup/SignupForm"; -import { makePageMetadata } from "@/seo/metadata"; - -export const metadata = { - ...makePageMetadata({ - title: "회원가입 — 약관 동의", - description: "PlanMate 회원가입 4단계: 약관 동의 페이지", - canonical: "/signup/terms", - }), - robots: { index: false, follow: false }, -}; - -export default function SignupTermsPage() { - return ( - <> - - 약관동의 - 서비스 이용을 위한 필수 약관입니다. - - - - - - - ); -} diff --git a/src/components/auth/AuthMain.tsx b/src/components/auth/AuthMain.tsx index 24892fe..5321d6a 100644 --- a/src/components/auth/AuthMain.tsx +++ b/src/components/auth/AuthMain.tsx @@ -1,6 +1,17 @@ -import type { AuthCommonProps } from "@/types/auth"; +"use client"; -export function AuthMain({ children }: AuthCommonProps) { +import { useSignupStepStore } from "@/stores/signupStepStore"; +import type { AuthMainProps } from "@/types/auth"; +import { useEffect } from "react"; + +export function AuthMain({ children, flow }: AuthMainProps) { + const resetSignupStep = useSignupStepStore((s) => s.reset); + + useEffect(() => { + if (flow === "signup") { + resetSignupStep(); + } + }, [flow, resetSignupStep]); return (
{children} diff --git a/src/components/auth/signup/StepTransitionWrapper.tsx b/src/components/auth/AuthStepTransition.tsx similarity index 73% rename from src/components/auth/signup/StepTransitionWrapper.tsx rename to src/components/auth/AuthStepTransition.tsx index d7bf117..73a83ef 100644 --- a/src/components/auth/signup/StepTransitionWrapper.tsx +++ b/src/components/auth/AuthStepTransition.tsx @@ -1,14 +1,8 @@ -"use client"; - import { authStepSlide, authTransition } from "@/lib/variants/motion.auth"; -import { StepTransitionWrapperProps } from "@/types/auth"; +import type { AuthStepTransitionProps } from "@/types/auth"; import { AnimatePresence, motion } from "framer-motion"; -export function StepTransitionWrapper({ - stepKey, - direction, - children, -}: StepTransitionWrapperProps) { +export function AuthStepTransition({ stepKey, direction, children }: AuthStepTransitionProps) { return ( index + 1); return ( diff --git a/src/components/auth/login/LoginForm.tsx b/src/components/auth/login/LoginForm.tsx index bba5c7c..59ecf4c 100644 --- a/src/components/auth/login/LoginForm.tsx +++ b/src/components/auth/login/LoginForm.tsx @@ -5,45 +5,14 @@ import { authFadeSlideUp, authTransition } from "@/lib/variants/motion.auth"; import { Button } from "@/shared/button"; import { Icon } from "@/shared/Icon"; import { Input } from "@/shared/input"; -import { useAuthFormStore } from "@/stores/authForm.store"; + import { motion } from "framer-motion"; import Link from "next/link"; -import type { ChangeEvent, FormEvent } from "react"; +import type { FormEvent } from "react"; export function LoginForm() { - const { - email, - password, - emailError, - passwordError, - isPasswordVisible, - setEmail, - setPassword, - clearEmailError, - clearPasswordError, - togglePasswordVisible, - validateLogin, - } = useAuthFormStore(); - const handleSubmit = (event: FormEvent) => { event.preventDefault(); // 기본 form submit 동작 막기 - - const isValid = validateLogin(); - if (!isValid) { - // 에러 상태/값 초기화는 스토어에서 이미 처리 - return; - } - - // TODO: 실제 로그인 요청 로직 - // ex) await login({ email, password }); - }; - - const handleEmailChange = (event: ChangeEvent) => { - setEmail(event.target.value); - }; - - const handlePasswordChange = (event: ChangeEvent) => { - setPassword(event.target.value); }; return ( @@ -62,66 +31,42 @@ export function LoginForm() { > {/* 이메일 필드 */}
-
- - - {emailError && ( - 이메일 정보를 확인해 주세요. - )} -
+
{/* 비밀번호 필드 + 표시 토글 */}
-
- - - {passwordError && ( - 비밀번호를 확인해 주세요. - )} -
+
- {/* 비밀번호 표시/숨김 토글 아이콘 */} + {/* 아이콘 버튼 (레이아웃만, 로직은 나중에) */}
@@ -156,7 +101,8 @@ export function LoginForm() { {/* 간편 로그인 + 소셜 버튼 */} -
+
+ {/* 구분선 + 텍스트 */}
간편 로그인 diff --git a/src/components/auth/signup/SignupEmailStep.tsx b/src/components/auth/signup/SignupEmailStep.tsx index 0da0b35..607ba3c 100644 --- a/src/components/auth/signup/SignupEmailStep.tsx +++ b/src/components/auth/signup/SignupEmailStep.tsx @@ -1,9 +1,9 @@ "use client"; import { Input } from "@/shared/input"; -import type { SignupFieldBaseProps } from "@/types/auth"; +import type { SignupStepFieldMeta } from "@/types/auth"; -export function SignupEmailStep({ fieldId, fieldName }: SignupFieldBaseProps) { +export function SignupEmailStep({ fieldId, fieldName }: SignupStepFieldMeta) { return (