From 7b44d8f50123b2c85f9f7c2bbac3e2031b13a133 Mon Sep 17 00:00:00 2001 From: Sage Date: Mon, 12 Jan 2026 10:54:30 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(auction):=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=EB=9E=99=ED=8B=B0=EB=B8=8C=20=EA=B0=80=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B6=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 다단계 구조와 애니메이션을 포함한 가이드북 기능 도입 - 신규 UI 컴포넌트, 데이터 모델, 네비게이션 로직 추가 - 애니메이션 처리를 위해 motion 라이브러리 추가 - guide 페이지에서 새로운 Guidebook을 사용하도록 업데이트 --- next.config.ts | 28 +- package.json | 1 + pnpm-lock.yaml | 60 ++ src/app/(public)/guide/page.tsx | 6 +- .../auction/hooks/use-auction-socket.tsx | 2 +- src/screens/guide/hooks/use-section-pager.ts | 101 +++ src/screens/guide/model/guide-data.ts | 108 +++ src/screens/guide/ui/guide-pages.tsx | 674 ++++++++++++++++++ src/screens/guide/ui/guide.tsx | 146 ++++ 9 files changed, 1113 insertions(+), 13 deletions(-) create mode 100644 src/screens/guide/hooks/use-section-pager.ts create mode 100644 src/screens/guide/model/guide-data.ts create mode 100644 src/screens/guide/ui/guide-pages.tsx create mode 100644 src/screens/guide/ui/guide.tsx diff --git a/next.config.ts b/next.config.ts index 9ae4dc70..f10d7a7a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,20 +4,30 @@ const nextConfig: NextConfig = { reactCompiler: true, images: { remotePatterns: [ - { protocol: "http", hostname: "k.kakaocdn.net" }, - { protocol: "http", hostname: "img1.kakaocdn.net" }, - { protocol: "https", hostname: "picsum.photos" }, - { protocol: "https", hostname: "images.unsplash.com" }, - { protocol: "https", hostname: "static.toss.im" }, - { protocol: "https", hostname: "windfall-bucket.s3.ap-northeast-2.amazonaws.com" }, - { protocol: "https", hostname: "wind-fall.store" }, + // Kakao { protocol: "http", hostname: "img1.kakaocdn.net" }, + { protocol: "http", hostname: "k.kakaocdn.net" }, + + // Naver { protocol: "http", hostname: "phinf.pstatic.net" }, - { protocol: "http", hostname: "ssl.pstatic.net" }, - { protocol: "http", hostname: "lh3.googleusercontent.com" }, { protocol: "https", hostname: "phinf.pstatic.net" }, + { protocol: "http", hostname: "ssl.pstatic.net" }, { protocol: "https", hostname: "ssl.pstatic.net" }, + + // Google + { protocol: "http", hostname: "lh3.googleusercontent.com" }, { protocol: "https", hostname: "lh3.googleusercontent.com" }, + + // Placeholder / Stock + { protocol: "https", hostname: "picsum.photos" }, + { protocol: "https", hostname: "images.unsplash.com" }, + + // Toss + { protocol: "https", hostname: "static.toss.im" }, + + // Windfall + { protocol: "https", hostname: "wind-fall.store" }, + { protocol: "https", hostname: "windfall-bucket.s3.ap-northeast-2.amazonaws.com" }, ], }, async rewrites() { diff --git a/package.json b/package.json index 88ab5664..b9b6b08a 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "dayjs": "^1.11.19", "embla-carousel-react": "^8.6.0", "lucide-react": "^0.556.0", + "motion": "^12.25.0", "nanoid": "^5.1.6", "next": "16.0.8", "next-themes": "^0.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fa7d0fa..8231f493 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ importers: lucide-react: specifier: ^0.556.0 version: 0.556.0(react@19.2.1) + motion: + specifier: ^12.25.0 + version: 12.25.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1) nanoid: specifier: ^5.1.6 version: 5.1.6 @@ -2691,6 +2694,20 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + framer-motion@12.25.0: + resolution: {integrity: sha512-mlWqd0rApIjeyhTCSNCqPYsUAEhkcUukZxH3ke6KbstBRPcxhEpuIjmiUQvB+1E9xkEm5SpNHBgHCapH/QHTWg==} + 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 + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3255,6 +3272,26 @@ packages: moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + motion-dom@12.24.11: + resolution: {integrity: sha512-DlWOmsXMJrV8lzZyd+LKjG2CXULUs++bkq8GZ2Sr0R0RRhs30K2wtY+LKiTjhmJU3W61HK+rB0GLz6XmPvTA1A==} + + motion-utils@12.24.10: + resolution: {integrity: sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==} + + motion@12.25.0: + resolution: {integrity: sha512-jBFohEYklpZ+TL64zv03sHdqr1Tsc8/yDy7u68hVzi7hTJYtv53AduchqCiY3aWi4vY1hweS8DWtgCuckusYdQ==} + 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 + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -6691,6 +6728,15 @@ snapshots: dependencies: is-callable: 1.2.7 + framer-motion@12.25.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + motion-dom: 12.24.11 + motion-utils: 12.24.10 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + fsevents@2.3.3: optional: true @@ -7199,6 +7245,20 @@ snapshots: moment@2.30.1: {} + motion-dom@12.24.11: + dependencies: + motion-utils: 12.24.10 + + motion-utils@12.24.10: {} + + motion@12.25.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + framer-motion: 12.25.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + ms@2.1.3: {} nano-spawn@2.0.0: {} diff --git a/src/app/(public)/guide/page.tsx b/src/app/(public)/guide/page.tsx index 32bab968..cf1afb5a 100644 --- a/src/app/(public)/guide/page.tsx +++ b/src/app/(public)/guide/page.tsx @@ -1,5 +1,5 @@ -import GuideScreen from "@/screens/guide/ui/guide-screen"; +import { Guidebook } from "@/screens/guide/ui/guide"; -export default function page() { - return ; +export default function Page() { + return ; } diff --git a/src/entities/auction/hooks/use-auction-socket.tsx b/src/entities/auction/hooks/use-auction-socket.tsx index 3437f45d..f1113c32 100644 --- a/src/entities/auction/hooks/use-auction-socket.tsx +++ b/src/entities/auction/hooks/use-auction-socket.tsx @@ -51,7 +51,7 @@ export default function useAuctionSocket(auctionId: string) { }, onDisconnect: () => { - console.log("[STOMP] disconnected"); + console.warn("[STOMP] disconnected"); }, onStompError: (frame) => { diff --git a/src/screens/guide/hooks/use-section-pager.ts b/src/screens/guide/hooks/use-section-pager.ts new file mode 100644 index 00000000..ac280a10 --- /dev/null +++ b/src/screens/guide/hooks/use-section-pager.ts @@ -0,0 +1,101 @@ +"use client"; + +import { useCallback, useEffect, useRef } from "react"; + +interface UseSectionPagerOptions { + pageCount: number; + currentIndex: number; + onChange: (nextIndex: number, direction: 1 | -1) => void; + lockMs?: number; + threshold?: number; +} + +export function useSectionPager({ + pageCount, + currentIndex, + onChange, + lockMs = 650, + threshold = 70, +}: UseSectionPagerOptions) { + const containerRef = useRef(null); + const lockRef = useRef(false); + const accRef = useRef(0); + + const commit = useCallback( + (next: number) => { + const clamped = Math.max(0, Math.min(pageCount - 1, next)); + if (clamped === currentIndex) return; + + const dir: 1 | -1 = clamped > currentIndex ? 1 : -1; + lockRef.current = true; + onChange(clamped, dir); + + window.setTimeout(() => { + lockRef.current = false; + accRef.current = 0; + }, lockMs); + }, + [currentIndex, lockMs, onChange, pageCount] + ); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + + const shouldIgnore = (target: EventTarget | null) => { + if (!(target instanceof HTMLElement)) return false; + + const tag = target.tagName.toLowerCase(); + if (tag === "input" || tag === "textarea" || tag === "select") return true; + if (target.isContentEditable) return true; + if (target.closest('[data-allow-scroll="true"]')) return true; + + return false; + }; + + const onWheel = (e: WheelEvent) => { + if (lockRef.current) return; + if (shouldIgnore(e.target)) return; + + e.preventDefault(); + accRef.current += e.deltaY; + + if (Math.abs(accRef.current) < threshold) return; + + if (accRef.current > 0) { + commit(currentIndex + 1); + } else { + commit(currentIndex - 1); + } + }; + + const onKeyDown = (e: KeyboardEvent) => { + if (lockRef.current) return; + if (shouldIgnore(document.activeElement)) return; + + if (e.key === "ArrowRight" || e.key === "PageDown") { + e.preventDefault(); + commit(currentIndex + 1); + } + if (e.key === "ArrowLeft" || e.key === "PageUp") { + e.preventDefault(); + commit(currentIndex - 1); + } + }; + + el.addEventListener("wheel", onWheel, { passive: false }); + window.addEventListener("keydown", onKeyDown); + + return () => { + el.removeEventListener("wheel", onWheel); + window.removeEventListener("keydown", onKeyDown); + }; + }, [commit, currentIndex, threshold]); + + return { + containerRef, + goNext: () => commit(currentIndex + 1), + goPrev: () => commit(currentIndex - 1), + goTo: (i: number) => commit(i), + }; +} diff --git a/src/screens/guide/model/guide-data.ts b/src/screens/guide/model/guide-data.ts new file mode 100644 index 00000000..2d21aafa --- /dev/null +++ b/src/screens/guide/model/guide-data.ts @@ -0,0 +1,108 @@ +import { + Calendar, + Clock, + DollarSign, + Package, + Shield, + TrendingDown, + Zap, + type LucideIcon, +} from "lucide-react"; + +export interface Keyword { + icon: LucideIcon; + label: string; + description: string; +} + +export const KEYWORDS: Keyword[] = [ + { icon: Clock, label: "시간", description: "시간이 흐르면서" }, + { icon: TrendingDown, label: "가격 하락", description: "가격이 점점 낮아지고" }, + { icon: Zap, label: "선점", description: "먼저 구매하는 사람이 승리" }, +]; + +export interface PricePoint { + time: string; + price: number; + label?: string; +} + +export const priceData: PricePoint[] = [ + { time: "00:00", price: 100000, label: "시작" }, + { time: "00:05", price: 95000 }, + { time: "00:10", price: 90000 }, + { time: "00:15", price: 85000 }, + { time: "00:20", price: 80000, label: "구매 완료" }, +]; + +export interface ChatMessage { + id: number; + text: string; + type: "thinking" | "waiting" | "success"; +} + +export const messages: ChatMessage[] = [ + { id: 1, text: "지금 사야 하나...?", type: "thinking" }, + { id: 2, text: "조금만 더 기다려볼까?", type: "thinking" }, + { id: 3, text: "다른 사람이 먼저 사면 어떡하지", type: "waiting" }, + { id: 4, text: "지금이다! 구매 완료!", type: "success" }, +]; + +export const priceSteps = [ + { time: "00:05", price: 90000 }, + { time: "00:10", price: 85000 }, + { time: "00:15", price: 80000 }, +]; + +export interface Step { + id: number; + icon: LucideIcon; + label: string; + placeholder: string; + description: string; +} + +export const steps: Step[] = [ + { + id: 1, + icon: Package, + label: "상품명", + placeholder: "예: 프리미엄 무선 이어폰", + description: "판매할 상품의 이름을 입력하세요", + }, + { + id: 2, + icon: DollarSign, + label: "시작 가격", + placeholder: "예: 100,000", + description: "경매가 시작되는 가격을 설정하세요", + }, + { + id: 3, + icon: TrendingDown, + label: "가격 하락 단위", + placeholder: "예: 5,000", + description: "5분마다 하락할 금액을 설정하세요", + }, + { + id: 4, + icon: Shield, + label: "Stop Loss", + placeholder: "예: 50,000", + description: "최대 하락 가격을 설정하세요", + }, + { + id: 5, + icon: Calendar, + label: "경매 시작 일정", + placeholder: "예: 2026-01-15 14:00", + description: "경매 시작 시간을 설정하세요", + }, +]; + +export const guideImages = { + liveAuction: + "https://images.unsplash.com/photo-1762553159827-7a5d2167b55d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx3aXJlbGVzcyUyMGVhcmJ1ZHMlMjBwcmVtaXVtfGVufDF8fHx8MTc2Nzk2NDY2OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral", + upcomingAuction: + "https://images.unsplash.com/photo-1745256375848-1d599594635d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbWFydCUyMHdhdGNoJTIwbW9kZXJufGVufDF8fHx8MTc2ODA2NjgzMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral", +}; diff --git a/src/screens/guide/ui/guide-pages.tsx b/src/screens/guide/ui/guide-pages.tsx new file mode 100644 index 00000000..7e19a25e --- /dev/null +++ b/src/screens/guide/ui/guide-pages.tsx @@ -0,0 +1,674 @@ +"use client"; + +import React, { useEffect, useState } from "react"; + +import Image from "next/image"; + +import { Calendar, CheckCircle, ChevronRight, Clock, TrendingDown } from "lucide-react"; +import { motion } from "motion/react"; + +import { KEYWORDS, guideImages, messages, priceData, priceSteps, steps } from "../model/guide-data"; + +export function GuidebookPage1() { + return ( +
+ + + 네덜란드 경매 + + + 시간이 지날수록 가격이 내려가는 독특한 경매 방식 + + + + + +
+ {KEYWORDS.map((keyword, index) => ( + + + + + + + {keyword.label} + + + + +

{keyword.description}

+
+ ))} +
+ + + 일반 경매와 달리, 높은 가격에서 시작해 점점 낮아집니다. +
+ 원하는 가격이 되면 빠르게 구매 결정을 내려야 합니다. +
+
+ ); +} + +export function GuidebookPage2() { + const [visibleBars, setVisibleBars] = useState(0); + + useEffect(() => { + const timers: NodeJS.Timeout[] = []; + priceData.forEach((_, index) => { + const timer = setTimeout(() => setVisibleBars(index + 1), index * 500 + 400); + timers.push(timer); + }); + return () => timers.forEach(clearTimeout); + }, []); + + return ( +
+ +

5분마다 일정 금액씩 가격이 하락합니다

+
+ +
+ {priceData.map((point, index) => ( + +
+ + {point.time} + + + {point.label && ( + + {point.label} + + )} +
+ + + + +
+ + ₩{point.price.toLocaleString()} + + + {index > 0 && ( + + -{Math.round((1 - point.price / priceData[0].price) * 100)}% + + )} +
+
+
+ ))} +
+ + +

+ 💡 원하는 가격이 되면 즉시 구매할 수 있습니다 +

+
+
+ ); +} + +export function GuidebookPage3() { + const [visibleMessages, setVisibleMessages] = useState(0); + + useEffect(() => { + const timers: NodeJS.Timeout[] = []; + messages.forEach((_, index) => { + const timer = setTimeout(() => setVisibleMessages(index + 1), index * 600 + 400); + timers.push(timer); + }); + return () => timers.forEach(clearTimeout); + }, []); + + return ( +
+ +

실시간 가격 변동

+
+ {priceSteps.map((step, index) => ( + +
+ {step.time} + + ₩{step.price.toLocaleString()} + +
+ + {index === 2 && ( + + 구매 가능한 가격! + + )} +
+ ))} +
+
+ + +

구매자의 고민

+
+ {messages.map((message, idx) => { + const isVisible = idx < visibleMessages; + let backgroundColor = "#F3F4F6"; + let textColor = "#1F2937"; + + if (message.type === "success") { + backgroundColor = "oklch(0.4758 0.2241 288.5)"; + textColor = "white"; + } else if (message.type === "waiting") { + backgroundColor = "#FEF3C7"; + } + + return ( + + +

{message.text}

+
+
+ ); + })} +
+ + {visibleMessages === messages.length && ( + +

+ 💡 타이밍을 잘 잡으면 원하는 가격에 구매할 수 있습니다 +

+
+ )} +
+
+ ); +} + +export function GuidebookPage4() { + return ( +
+ +

경매 상태를 확인하고 적절한 타이밍에 참여하세요

+
+ +
+ +
+ Live Auction + + + LIVE + +
+ +
+

프리미엄 무선 이어폰

+ + +
+ 현재 가격 + +
+
+ ₩85,000 +
+
₩120,000
+
+ +
+ + 다음 가격 하락: 00:04:30 +
+ + + 지금 구매하기 + +
+
+ + +
+ Upcoming Auction + + COMING SOON + +
+ +
+

스마트 워치 프로

+ +
+
시작 가격
+
₩450,000
+
+ +
+ + 시작: 2026-01-15 14:00 +
+ + + 알림 받기 + +
+
+
+
+ ); +} + +export function GuidebookPage5() { + const [currentStep, setCurrentStep] = useState(0); + const [completedSteps, setCompletedSteps] = useState([]); + + const handleComplete = () => { + setCompletedSteps((prev) => (prev.includes(currentStep) ? prev : [...prev, currentStep])); + if (currentStep < steps.length - 1) setCurrentStep((s) => s + 1); + }; + + return ( +
+ +

5단계로 간편하게 경매를 등록하세요

+
+ +
+ {steps.map((step, index) => { + const isCompleted = completedSteps.includes(index); + const isCurrent = index === currentStep; + const borderColor = isCompleted || isCurrent ? "oklch(0.4758 0.2241 288.5)" : "#E5E7EB"; + let backgroundColor = "white"; + + if (isCompleted) { + backgroundColor = "oklch(0.4758 0.2241 288.5)"; + } else if (isCurrent) { + backgroundColor = "oklch(0.4758 0.2241 288.5 / 0.1)"; + } + + const labelColor = isCompleted || isCurrent ? "oklch(0.4758 0.2241 288.5)" : "#9CA3AF"; + const iconColor = isCurrent ? "oklch(0.4758 0.2241 288.5)" : "#9CA3AF"; + + return ( + + setCurrentStep(index)} + className="flex flex-col items-center gap-2 transition-all" + initial={{ opacity: 0, scale: 0.8 }} + animate={{ opacity: 1, scale: 1 }} + transition={{ delay: index * 0.1 }} + whileHover={{ scale: 1.05 }} + > + + {isCompleted ? ( + + ) : ( + React.createElement(step.icon, { + className: "w-7 h-7", + style: { + color: iconColor, + }, + }) + )} + + + Step {index + 1} + + + + {index < steps.length - 1 && ( +
+ )} + + ); + })} +
+ + +
+
+
+ {React.createElement(steps[currentStep].icon, { + className: "w-6 h-6", + style: { color: "oklch(0.4758 0.2241 288.5)" }, + })} +
+
+

{steps[currentStep].label}

+

{steps[currentStep].description}

+
+
+ + + + + {currentStep < steps.length - 1 ? "다음 단계" : "경매 등록하기"} + + +
+
+ + {completedSteps.length === steps.length && ( + +
+ + 모든 단계 완료! +
+
+ )} +
+ ); +} diff --git a/src/screens/guide/ui/guide.tsx b/src/screens/guide/ui/guide.tsx new file mode 100644 index 00000000..c96d725c --- /dev/null +++ b/src/screens/guide/ui/guide.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { useMemo, useState } from "react"; + +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; + +import { + GuidebookPage1, + GuidebookPage2, + GuidebookPage3, + GuidebookPage4, + GuidebookPage5, +} from "./guide-pages"; +import { useSectionPager } from "../hooks/use-section-pager"; + +export function Guidebook() { + const [currentPage, setCurrentPage] = useState(0); + const [direction, setDirection] = useState<1 | -1>(1); + + const pages = useMemo( + () => [ + { title: "네덜란드 경매란?", node: }, + { title: "가격은 이렇게 내려갑니다", node: }, + { title: "구매자의 선택", node: }, + { title: "구매자 가이드", node: }, + { title: "판매자 가이드", node: }, + ], + [] + ); + + const totalPages = pages.length; + + const { containerRef, goNext, goPrev, goTo } = useSectionPager({ + pageCount: totalPages, + currentIndex: currentPage, + onChange: (next, dir) => { + setDirection(dir); + setCurrentPage(next); + }, + lockMs: 650, + threshold: 70, + }); + + const pageVariants = { + enter: (dir: number) => ({ x: dir > 0 ? 1000 : -1000, opacity: 0, scale: 0.95 }), + center: { x: 0, opacity: 1, scale: 1 }, + exit: (dir: number) => ({ x: dir > 0 ? -1000 : 1000, opacity: 0, scale: 0.95 }), + }; + + return ( +
+
+
+ +

+ Chapter {currentPage + 1} / {totalPages} +

+

{pages[currentPage].title}

+
+ +
+
+ + + {pages[currentPage].node} + + +
+
+ +
+ + + 이전 + + +
+ {pages.map((page, index) => ( + goTo(index)} + className="rounded-full transition-all" + style={{ + width: index === currentPage ? "32px" : "8px", + height: "8px", + backgroundColor: + index === currentPage ? "oklch(0.4758 0.2241 288.5)" : "#D1D5DB", + }} + whileHover={{ scale: 1.2 }} + layout + aria-label={`페이지 ${index + 1}로 이동`} + /> + ))} +
+ + + 다음 + + +
+
+
+
+ ); +} From 39d3638459a1bba779cda61a3a81d896f9b6d660 Mon Sep 17 00:00:00 2001 From: Sage Date: Mon, 12 Jan 2026 11:14:09 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(guide):=20=EC=B2=AB=20=EB=B0=A9?= =?UTF-8?q?=EB=AC=B8=20=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=A7=84=EC=9E=85=20?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?UX=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - localStorage 키를 기반으로 첫 방문 사용자를 guide로 리다이렉트하는 GuideFirstVisitGate 도입 - 가이드에 skip 버튼을 추가해 사용자가 흐름을 건너뛸 수 있도록 개선 - guide 관련 localStorage 키를 상수로 중앙화하여 관리 - input 및 search input 스타일에서 box-shadow를 제거하고 일관된 외형으로 정리 --- src/app/layout.tsx | 2 ++ src/screens/guide/model/guide-constants.ts | 1 + src/screens/guide/ui/guide.tsx | 27 ++++++++++++++++++-- src/shared/ui/input/input.tsx | 2 +- src/shared/ui/input/search-input.tsx | 7 ++--- src/widgets/guide/guide-first-visit-gate.tsx | 25 ++++++++++++++++++ 6 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 src/screens/guide/model/guide-constants.ts create mode 100644 src/widgets/guide/guide-first-visit-gate.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 757d1e9b..9ccb70d1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -12,6 +12,7 @@ import { ServerTimeStoreProvider } from "@/shared/lib/providers/server-time-stor import "@/shared/styles/globals.css"; import { ToastRegistry } from "@/shared/ui/toast/toast-registry"; import { BottomNav } from "@/widgets/bottom-nav"; +import { GuideFirstVisitGate } from "@/widgets/guide/guide-first-visit-gate"; import { Header } from "@/widgets/header/header"; const inter = Inter({ @@ -41,6 +42,7 @@ export default function RootLayout({ +
{children}
diff --git a/src/screens/guide/model/guide-constants.ts b/src/screens/guide/model/guide-constants.ts new file mode 100644 index 00000000..b1735538 --- /dev/null +++ b/src/screens/guide/model/guide-constants.ts @@ -0,0 +1 @@ +export const GUIDE_STORAGE_KEY = "windfall_guide_seen"; diff --git a/src/screens/guide/ui/guide.tsx b/src/screens/guide/ui/guide.tsx index c96d725c..39a584f7 100644 --- a/src/screens/guide/ui/guide.tsx +++ b/src/screens/guide/ui/guide.tsx @@ -1,6 +1,8 @@ "use client"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; + +import { useRouter } from "next/navigation"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; @@ -13,8 +15,10 @@ import { GuidebookPage5, } from "./guide-pages"; import { useSectionPager } from "../hooks/use-section-pager"; +import { GUIDE_STORAGE_KEY } from "../model/guide-constants"; export function Guidebook() { + const router = useRouter(); const [currentPage, setCurrentPage] = useState(0); const [direction, setDirection] = useState<1 | -1>(1); @@ -48,10 +52,29 @@ export function Guidebook() { exit: (dir: number) => ({ x: dir > 0 ? -1000 : 1000, opacity: 0, scale: 0.95 }), }; + useEffect(() => { + if (typeof window === "undefined") return; + window.localStorage.setItem(GUIDE_STORAGE_KEY, "true"); + }, []); + return (
-
+
+ { + if (typeof window !== "undefined") { + window.localStorage.setItem(GUIDE_STORAGE_KEY, "true"); + } + router.replace("/"); + }} + > + Skip + @@ -34,7 +35,7 @@ export default function SearchInput({ type="text" className={cn( "flex-1", - "border-none bg-transparent focus-visible:ring-0 dark:bg-transparent", + "border-none bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent", className )} value={value} diff --git a/src/widgets/guide/guide-first-visit-gate.tsx b/src/widgets/guide/guide-first-visit-gate.tsx new file mode 100644 index 00000000..487616db --- /dev/null +++ b/src/widgets/guide/guide-first-visit-gate.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { useEffect } from "react"; + +import { usePathname, useRouter } from "next/navigation"; + +import { GUIDE_STORAGE_KEY } from "@/screens/guide/model/guide-constants"; +import { ROUTES } from "@/shared/config/routes"; + +export function GuideFirstVisitGate() { + const router = useRouter(); + const pathname = usePathname(); + + useEffect(() => { + if (pathname === ROUTES.guide) return; + if (typeof window === "undefined") return; + + const hasSeen = window.localStorage.getItem(GUIDE_STORAGE_KEY) === "true"; + if (hasSeen) return; + + router.replace(ROUTES.guide); + }, [pathname, router]); + + return null; +} From dfa50666512209680d7ea9e679c3339318836453 Mon Sep 17 00:00:00 2001 From: Sage Date: Mon, 12 Jan 2026 11:48:22 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore(guide):=20=EA=B0=80=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B6=81=20=EB=8B=A4=ED=81=AC=20=EB=AA=A8=EB=93=9C=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/screens/guide/ui/guide-pages.tsx | 49 ++++++++++++++-------------- src/screens/guide/ui/guide.tsx | 4 +-- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/screens/guide/ui/guide-pages.tsx b/src/screens/guide/ui/guide-pages.tsx index 7e19a25e..78a60ee3 100644 --- a/src/screens/guide/ui/guide-pages.tsx +++ b/src/screens/guide/ui/guide-pages.tsx @@ -112,7 +112,7 @@ export function GuidebookPage2() { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6 }} > -

5분마다 일정 금액씩 가격이 하락합니다

+

5분마다 일정 금액씩 가격이 하락합니다

@@ -271,7 +271,7 @@ export function GuidebookPage3() { animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.6 }} > -

실시간 가격 변동

+

실시간 가격 변동

{priceSteps.map((step, index) => ( -

구매자의 고민

+

구매자의 고민

{messages.map((message, idx) => { const isVisible = idx < visibleMessages; @@ -378,7 +378,9 @@ export function GuidebookPage4() { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6 }} > -

경매 상태를 확인하고 적절한 타이밍에 참여하세요

+

+ 경매 상태를 확인하고 적절한 타이밍에 참여하세요 +

@@ -386,9 +388,9 @@ export function GuidebookPage4() { initial={{ opacity: 0, x: -40 }} animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.6, delay: 0.2 }} - className="overflow-hidden rounded-2xl bg-gray-50 shadow-lg" + className="bg-secondary/60 border-border/60 overflow-hidden rounded-2xl border shadow-lg" > -
+
Live Auction프리미엄 무선 이어폰
- 현재 가격 + 현재 가격
₩85,000
-
₩120,000
+
₩120,000
-
+
다음 가격 하락: 00:04:30
@@ -461,9 +463,9 @@ export function GuidebookPage4() { initial={{ opacity: 0, x: 40 }} animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.6, delay: 0.4 }} - className="overflow-hidden rounded-2xl bg-gray-50 shadow-lg" + className="bg-secondary/60 border-border/60 overflow-hidden rounded-2xl border shadow-lg" > -
+
Upcoming Auction

스마트 워치 프로

-
-
시작 가격
-
₩450,000
+
+
시작 가격
+
₩450,000
-
+
시작: 2026-01-15 14:00
알림 받기 @@ -528,7 +527,7 @@ export function GuidebookPage5() { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6 }} > -

5단계로 간편하게 경매를 등록하세요

+

5단계로 간편하게 경매를 등록하세요

@@ -611,7 +610,7 @@ export function GuidebookPage5() { transition={{ duration: 0.3 }} className="mx-auto w-full max-w-xl flex-1" > -
+

{steps[currentStep].label}

-

{steps[currentStep].description}

+

{steps[currentStep].description}

diff --git a/src/screens/guide/ui/guide.tsx b/src/screens/guide/ui/guide.tsx index 39a584f7..85fe6e10 100644 --- a/src/screens/guide/ui/guide.tsx +++ b/src/screens/guide/ui/guide.tsx @@ -58,7 +58,7 @@ export function Guidebook() { }, []); return ( -
+
{pages[currentPage].title} -
+
Date: Mon, 12 Jan 2026 12:04:12 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor(guide):=20=EA=B0=80=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=EB=B6=81=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A5=BC=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 guide-pages.tsx의 대형 파일을 pages/ 디렉터리 하위의 5개 개별 페이지 컴포넌트로 분리 - guide-pages.tsx에서는 신규 페이지 컴포넌트를 재-export하도록 수정 --- src/screens/guide/ui/guide-pages.tsx | 678 +----------------- .../guide/ui/pages/guidebook-page1.tsx | 88 +++ .../guide/ui/pages/guidebook-page2.tsx | 167 +++++ .../guide/ui/pages/guidebook-page3.tsx | 124 ++++ .../guide/ui/pages/guidebook-page4.tsx | 149 ++++ .../guide/ui/pages/guidebook-page5.tsx | 170 +++++ 6 files changed, 703 insertions(+), 673 deletions(-) create mode 100644 src/screens/guide/ui/pages/guidebook-page1.tsx create mode 100644 src/screens/guide/ui/pages/guidebook-page2.tsx create mode 100644 src/screens/guide/ui/pages/guidebook-page3.tsx create mode 100644 src/screens/guide/ui/pages/guidebook-page4.tsx create mode 100644 src/screens/guide/ui/pages/guidebook-page5.tsx diff --git a/src/screens/guide/ui/guide-pages.tsx b/src/screens/guide/ui/guide-pages.tsx index 78a60ee3..fa9f6039 100644 --- a/src/screens/guide/ui/guide-pages.tsx +++ b/src/screens/guide/ui/guide-pages.tsx @@ -1,673 +1,5 @@ -"use client"; - -import React, { useEffect, useState } from "react"; - -import Image from "next/image"; - -import { Calendar, CheckCircle, ChevronRight, Clock, TrendingDown } from "lucide-react"; -import { motion } from "motion/react"; - -import { KEYWORDS, guideImages, messages, priceData, priceSteps, steps } from "../model/guide-data"; - -export function GuidebookPage1() { - return ( -
- - - 네덜란드 경매 - - - 시간이 지날수록 가격이 내려가는 독특한 경매 방식 - - - - - -
- {KEYWORDS.map((keyword, index) => ( - - - - - - - {keyword.label} - - - - -

{keyword.description}

-
- ))} -
- - - 일반 경매와 달리, 높은 가격에서 시작해 점점 낮아집니다. -
- 원하는 가격이 되면 빠르게 구매 결정을 내려야 합니다. -
-
- ); -} - -export function GuidebookPage2() { - const [visibleBars, setVisibleBars] = useState(0); - - useEffect(() => { - const timers: NodeJS.Timeout[] = []; - priceData.forEach((_, index) => { - const timer = setTimeout(() => setVisibleBars(index + 1), index * 500 + 400); - timers.push(timer); - }); - return () => timers.forEach(clearTimeout); - }, []); - - return ( -
- -

5분마다 일정 금액씩 가격이 하락합니다

-
- -
- {priceData.map((point, index) => ( - -
- - {point.time} - - - {point.label && ( - - {point.label} - - )} -
- - - - -
- - ₩{point.price.toLocaleString()} - - - {index > 0 && ( - - -{Math.round((1 - point.price / priceData[0].price) * 100)}% - - )} -
-
-
- ))} -
- - -

- 💡 원하는 가격이 되면 즉시 구매할 수 있습니다 -

-
-
- ); -} - -export function GuidebookPage3() { - const [visibleMessages, setVisibleMessages] = useState(0); - - useEffect(() => { - const timers: NodeJS.Timeout[] = []; - messages.forEach((_, index) => { - const timer = setTimeout(() => setVisibleMessages(index + 1), index * 600 + 400); - timers.push(timer); - }); - return () => timers.forEach(clearTimeout); - }, []); - - return ( -
- -

실시간 가격 변동

-
- {priceSteps.map((step, index) => ( - -
- {step.time} - - ₩{step.price.toLocaleString()} - -
- - {index === 2 && ( - - 구매 가능한 가격! - - )} -
- ))} -
-
- - -

구매자의 고민

-
- {messages.map((message, idx) => { - const isVisible = idx < visibleMessages; - let backgroundColor = "#F3F4F6"; - let textColor = "#1F2937"; - - if (message.type === "success") { - backgroundColor = "oklch(0.4758 0.2241 288.5)"; - textColor = "white"; - } else if (message.type === "waiting") { - backgroundColor = "#FEF3C7"; - } - - return ( - - -

{message.text}

-
-
- ); - })} -
- - {visibleMessages === messages.length && ( - -

- 💡 타이밍을 잘 잡으면 원하는 가격에 구매할 수 있습니다 -

-
- )} -
-
- ); -} - -export function GuidebookPage4() { - return ( -
- -

- 경매 상태를 확인하고 적절한 타이밍에 참여하세요 -

-
- -
- -
- Live Auction - - - LIVE - -
- -
-

프리미엄 무선 이어폰

- - -
- 현재 가격 - -
-
- ₩85,000 -
-
₩120,000
-
- -
- - 다음 가격 하락: 00:04:30 -
- - - 지금 구매하기 - -
-
- - -
- Upcoming Auction - - COMING SOON - -
- -
-

스마트 워치 프로

- -
-
시작 가격
-
₩450,000
-
- -
- - 시작: 2026-01-15 14:00 -
- - - 알림 받기 - -
-
-
-
- ); -} - -export function GuidebookPage5() { - const [currentStep, setCurrentStep] = useState(0); - const [completedSteps, setCompletedSteps] = useState([]); - - const handleComplete = () => { - setCompletedSteps((prev) => (prev.includes(currentStep) ? prev : [...prev, currentStep])); - if (currentStep < steps.length - 1) setCurrentStep((s) => s + 1); - }; - - return ( -
- -

5단계로 간편하게 경매를 등록하세요

-
- -
- {steps.map((step, index) => { - const isCompleted = completedSteps.includes(index); - const isCurrent = index === currentStep; - const borderColor = isCompleted || isCurrent ? "oklch(0.4758 0.2241 288.5)" : "#E5E7EB"; - let backgroundColor = "white"; - - if (isCompleted) { - backgroundColor = "oklch(0.4758 0.2241 288.5)"; - } else if (isCurrent) { - backgroundColor = "oklch(0.4758 0.2241 288.5 / 0.1)"; - } - - const labelColor = isCompleted || isCurrent ? "oklch(0.4758 0.2241 288.5)" : "#9CA3AF"; - const iconColor = isCurrent ? "oklch(0.4758 0.2241 288.5)" : "#9CA3AF"; - - return ( - - setCurrentStep(index)} - className="flex flex-col items-center gap-2 transition-all" - initial={{ opacity: 0, scale: 0.8 }} - animate={{ opacity: 1, scale: 1 }} - transition={{ delay: index * 0.1 }} - whileHover={{ scale: 1.05 }} - > - - {isCompleted ? ( - - ) : ( - React.createElement(step.icon, { - className: "w-7 h-7", - style: { - color: iconColor, - }, - }) - )} - - - Step {index + 1} - - - - {index < steps.length - 1 && ( -
- )} - - ); - })} -
- - -
-
-
- {React.createElement(steps[currentStep].icon, { - className: "w-6 h-6", - style: { color: "oklch(0.4758 0.2241 288.5)" }, - })} -
-
-

{steps[currentStep].label}

-

{steps[currentStep].description}

-
-
- - - - - {currentStep < steps.length - 1 ? "다음 단계" : "경매 등록하기"} - - -
-
- - {completedSteps.length === steps.length && ( - -
- - 모든 단계 완료! -
-
- )} -
- ); -} +export { GuidebookPage1 } from "./pages/guidebook-page1"; +export { GuidebookPage2 } from "./pages/guidebook-page2"; +export { GuidebookPage3 } from "./pages/guidebook-page3"; +export { GuidebookPage4 } from "./pages/guidebook-page4"; +export { GuidebookPage5 } from "./pages/guidebook-page5"; diff --git a/src/screens/guide/ui/pages/guidebook-page1.tsx b/src/screens/guide/ui/pages/guidebook-page1.tsx new file mode 100644 index 00000000..948a16cd --- /dev/null +++ b/src/screens/guide/ui/pages/guidebook-page1.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { motion } from "motion/react"; + +import { KEYWORDS } from "../../model/guide-data"; + +export function GuidebookPage1() { + return ( +
+ + + 네덜란드 경매 + + + 시간이 지날수록 가격이 내려가는 독특한 경매 방식 + + + + + +
+ {KEYWORDS.map((keyword, index) => ( + + + + + + + {keyword.label} + + + + +

{keyword.description}

+
+ ))} +
+ + + 일반 경매와 달리, 높은 가격에서 시작해 점점 낮아집니다. +
+ 원하는 가격이 되면 빠르게 구매 결정을 내려야 합니다. +
+
+ ); +} diff --git a/src/screens/guide/ui/pages/guidebook-page2.tsx b/src/screens/guide/ui/pages/guidebook-page2.tsx new file mode 100644 index 00000000..b5bb9876 --- /dev/null +++ b/src/screens/guide/ui/pages/guidebook-page2.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { motion } from "motion/react"; + +import { priceData } from "../../model/guide-data"; + +export function GuidebookPage2() { + const [visibleBars, setVisibleBars] = useState(0); + + useEffect(() => { + const timers: NodeJS.Timeout[] = []; + priceData.forEach((_, index) => { + const timer = setTimeout(() => setVisibleBars(index + 1), index * 500 + 400); + timers.push(timer); + }); + return () => timers.forEach(clearTimeout); + }, []); + + return ( +
+ +

5분마다 일정 금액씩 가격이 하락합니다

+
+ +
+ {priceData.map((point, index) => ( + +
+ + {point.time} + + + {point.label && ( + + {point.label} + + )} +
+ + + + +
+ + ₩{point.price.toLocaleString()} + + + {index > 0 && ( + + -{Math.round((1 - point.price / priceData[0].price) * 100)}% + + )} +
+
+
+ ))} +
+ + +

+ 💡 원하는 가격이 되면 즉시 구매할 수 있습니다 +

+
+
+ ); +} diff --git a/src/screens/guide/ui/pages/guidebook-page3.tsx b/src/screens/guide/ui/pages/guidebook-page3.tsx new file mode 100644 index 00000000..0910908a --- /dev/null +++ b/src/screens/guide/ui/pages/guidebook-page3.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { motion } from "motion/react"; + +import { messages, priceSteps } from "../../model/guide-data"; + +export function GuidebookPage3() { + const [visibleMessages, setVisibleMessages] = useState(0); + + useEffect(() => { + const timers: NodeJS.Timeout[] = []; + messages.forEach((_, index) => { + const timer = setTimeout(() => setVisibleMessages(index + 1), index * 600 + 400); + timers.push(timer); + }); + return () => timers.forEach(clearTimeout); + }, []); + + return ( +
+ +

실시간 가격 변동

+
+ {priceSteps.map((step, index) => ( + +
+ {step.time} + + ₩{step.price.toLocaleString()} + +
+ + {index === 2 && ( + + 구매 가능한 가격! + + )} +
+ ))} +
+
+ + +

구매자의 고민

+
+ {messages.map((message, idx) => { + const isVisible = idx < visibleMessages; + let backgroundColor = "#F3F4F6"; + let textColor = "#1F2937"; + + if (message.type === "success") { + backgroundColor = "oklch(0.4758 0.2241 288.5)"; + textColor = "white"; + } else if (message.type === "waiting") { + backgroundColor = "#FEF3C7"; + } + + return ( + + +

{message.text}

+
+
+ ); + })} +
+ + {visibleMessages === messages.length && ( + +

+ 💡 타이밍을 잘 잡으면 원하는 가격에 구매할 수 있습니다 +

+
+ )} +
+
+ ); +} diff --git a/src/screens/guide/ui/pages/guidebook-page4.tsx b/src/screens/guide/ui/pages/guidebook-page4.tsx new file mode 100644 index 00000000..7ad0b842 --- /dev/null +++ b/src/screens/guide/ui/pages/guidebook-page4.tsx @@ -0,0 +1,149 @@ +"use client"; + +import Image from "next/image"; + +import { Calendar, Clock, TrendingDown } from "lucide-react"; +import { motion } from "motion/react"; + +import { guideImages } from "../../model/guide-data"; + +export function GuidebookPage4() { + return ( +
+ +

+ 경매 상태를 확인하고 적절한 타이밍에 참여하세요 +

+
+ +
+ +
+ Live Auction + + + LIVE + +
+ +
+

프리미엄 무선 이어폰

+ + +
+ 현재 가격 + +
+
+ ₩85,000 +
+
₩120,000
+
+ +
+ + 다음 가격 하락: 00:04:30 +
+ + + 지금 구매하기 + +
+
+ + +
+ Upcoming Auction + + COMING SOON + +
+ +
+

스마트 워치 프로

+ +
+
시작 가격
+
₩450,000
+
+ +
+ + 시작: 2026-01-15 14:00 +
+ + + 알림 받기 + +
+
+
+
+ ); +} diff --git a/src/screens/guide/ui/pages/guidebook-page5.tsx b/src/screens/guide/ui/pages/guidebook-page5.tsx new file mode 100644 index 00000000..4e9c6753 --- /dev/null +++ b/src/screens/guide/ui/pages/guidebook-page5.tsx @@ -0,0 +1,170 @@ +"use client"; + +import React, { useState } from "react"; + +import { CheckCircle, ChevronRight } from "lucide-react"; +import { motion } from "motion/react"; + +import { steps } from "../../model/guide-data"; + +export function GuidebookPage5() { + const [currentStep, setCurrentStep] = useState(0); + const [completedSteps, setCompletedSteps] = useState([]); + + const handleComplete = () => { + setCompletedSteps((prev) => (prev.includes(currentStep) ? prev : [...prev, currentStep])); + if (currentStep < steps.length - 1) setCurrentStep((s) => s + 1); + }; + + return ( +
+ +

5단계로 간편하게 경매를 등록하세요

+
+ +
+ {steps.map((step, index) => { + const isCompleted = completedSteps.includes(index); + const isCurrent = index === currentStep; + const borderColor = isCompleted || isCurrent ? "oklch(0.4758 0.2241 288.5)" : "#E5E7EB"; + let backgroundColor = "white"; + + if (isCompleted) { + backgroundColor = "oklch(0.4758 0.2241 288.5)"; + } else if (isCurrent) { + backgroundColor = "oklch(0.4758 0.2241 288.5 / 0.1)"; + } + + const labelColor = isCompleted || isCurrent ? "oklch(0.4758 0.2241 288.5)" : "#9CA3AF"; + const iconColor = isCurrent ? "oklch(0.4758 0.2241 288.5)" : "#9CA3AF"; + + return ( + + setCurrentStep(index)} + className="flex flex-col items-center gap-2 transition-all" + initial={{ opacity: 0, scale: 0.8 }} + animate={{ opacity: 1, scale: 1 }} + transition={{ delay: index * 0.1 }} + whileHover={{ scale: 1.05 }} + > + + {isCompleted ? ( + + ) : ( + React.createElement(steps[currentStep].icon, { + className: "w-7 h-7", + style: { + color: iconColor, + }, + }) + )} + + + Step {index + 1} + + + + {index < steps.length - 1 && ( +
+ )} + + ); + })} +
+ + +
+
+
+ {React.createElement(steps[currentStep].icon, { + className: "w-6 h-6", + style: { color: "oklch(0.4758 0.2241 288.5)" }, + })} +
+
+

{steps[currentStep].label}

+

{steps[currentStep].description}

+
+
+ + + + + {currentStep < steps.length - 1 ? "다음 단계" : "경매 등록하기"} + + +
+
+ + {completedSteps.length === steps.length && ( + +
+ + 모든 단계 완료! +
+
+ )} +
+ ); +}