116 lines
4.0 KiB
TypeScript
116 lines
4.0 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import React, { useEffect, useState } from "react";
|
||
|
|
import { motion, AnimatePresence } from "framer-motion";
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
|
||
|
|
const TONE_PHRASES = [
|
||
|
|
{ q: "주식이 너무 어려워요...", a: "걱정하지 마. JOORIN-E가 다 해줄게." },
|
||
|
|
{
|
||
|
|
q: "내 돈, 정말 안전할까?",
|
||
|
|
a: "안심해도 돼. 금융권 수준 보안키로 지키니까.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
q: "손실 날까 봐 불안해요...",
|
||
|
|
a: "걱정하지 마. 안전 장치가 24시간 작동해.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
q: "복잡한 건 딱 질색인데..",
|
||
|
|
a: "몰라도 돼. 클릭 몇 번이면 바로 시작이야.",
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @description 프리미엄한 텍스트 리빌 효과를 제공하는 브랜드 톤 컴포넌트
|
||
|
|
*/
|
||
|
|
export function AnimatedBrandTone() {
|
||
|
|
const [index, setIndex] = useState(0);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const timer = setInterval(() => {
|
||
|
|
setIndex((prev) => (prev + 1) % TONE_PHRASES.length);
|
||
|
|
}, 5000);
|
||
|
|
|
||
|
|
return () => clearInterval(timer);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex min-h-[300px] flex-col items-center justify-center py-10 text-center md:min-h-[400px]">
|
||
|
|
<AnimatePresence mode="wait">
|
||
|
|
<motion.div
|
||
|
|
key={index}
|
||
|
|
initial={{ opacity: 0, y: 10 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
exit={{ opacity: 0, y: -10 }}
|
||
|
|
transition={{ duration: 0.5, ease: "easeOut" }}
|
||
|
|
className="flex flex-col items-center w-full"
|
||
|
|
>
|
||
|
|
{/* 질문 (Q) */}
|
||
|
|
<motion.p
|
||
|
|
initial={{ opacity: 0 }}
|
||
|
|
animate={{ opacity: 1 }}
|
||
|
|
transition={{ delay: 0.2 }}
|
||
|
|
className="text-sm font-medium text-brand-300/60 md:text-lg"
|
||
|
|
>
|
||
|
|
“{TONE_PHRASES[index].q}”
|
||
|
|
</motion.p>
|
||
|
|
|
||
|
|
{/* 답변 (A) - 타이핑 효과 */}
|
||
|
|
<div className="mt-8 flex flex-col items-center gap-2">
|
||
|
|
<h2 className="text-4xl font-extrabold tracking-wide text-white drop-shadow-lg md:text-6xl lg:text-7xl">
|
||
|
|
<div className="inline-block break-keep whitespace-pre-wrap leading-tight">
|
||
|
|
{TONE_PHRASES[index].a.split("").map((char, i) => (
|
||
|
|
<motion.span
|
||
|
|
key={i}
|
||
|
|
initial={{ opacity: 0 }}
|
||
|
|
animate={{ opacity: 1 }}
|
||
|
|
transition={{
|
||
|
|
duration: 0,
|
||
|
|
delay: 0.5 + i * 0.1, // 글자당 0.1초 딜레이로 타이핑 효과
|
||
|
|
}}
|
||
|
|
className={cn(
|
||
|
|
"inline-block",
|
||
|
|
// 앞부분 강조 색상 로직은 단순화하거나 유지 (여기서는 전체 텍스트 톤 유지)
|
||
|
|
i < 5 ? "text-brand-300" : "text-white",
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{char === " " ? "\u00A0" : char}
|
||
|
|
</motion.span>
|
||
|
|
))}
|
||
|
|
{/* 깜빡이는 커서 */}
|
||
|
|
<motion.span
|
||
|
|
initial={{ opacity: 0 }}
|
||
|
|
animate={{ opacity: [0, 1, 0] }}
|
||
|
|
transition={{
|
||
|
|
duration: 0.8,
|
||
|
|
repeat: Infinity,
|
||
|
|
ease: "linear",
|
||
|
|
}}
|
||
|
|
className="ml-1 inline-block h-[0.8em] w-1.5 bg-brand-400 align-middle shadow-[0_0_10px_rgba(45,212,191,0.5)]"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</h2>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
</AnimatePresence>
|
||
|
|
|
||
|
|
{/* 인디케이터 - 유휴 상태 표시 및 선택 기능 */}
|
||
|
|
<div className="mt-16 flex gap-3">
|
||
|
|
{TONE_PHRASES.map((_, i) => (
|
||
|
|
<button
|
||
|
|
key={i}
|
||
|
|
onClick={() => setIndex(i)}
|
||
|
|
className={cn(
|
||
|
|
"h-1.5 transition-all duration-500 rounded-full",
|
||
|
|
i === index
|
||
|
|
? "w-10 bg-brand-500 shadow-[0_0_20px_rgba(20,184,166,0.3)]"
|
||
|
|
: "w-2 bg-white/10 hover:bg-white/20",
|
||
|
|
)}
|
||
|
|
aria-label={`Go to slide ${i + 1}`}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|