import { useCallback, useEffect, useRef, useState } from "react"; // 페이지 맨 위 파도타기 — 5마리가 순차로 만세(팔 올림)했다 내린다. // 5마리는 한 그룹으로 묶여 함께 끌어 옮길 수 있고, 각 캐릭터를 탭하면 // 남아공전 응원 말풍선이 뜬다. type Animal = { name: string; lines: string[] }; const WAVE: Animal[] = [ { name: "bull", lines: [ "한국, 남아공 잡고 16강 가자! 🇰🇷", "남아공 수비를 뚫어라! 🐂", "오늘 이기면 16강이 보인다", "대~한민국! 짝짝짝 👏", "투우사도 한국 응원한다 ⚽", "스페인도 한국 편이다 🇪🇸", "남아공 골키퍼 긴장해라!", "한 골이 16강을 가른다", "투지로 밀어붙여! 💪", "끝까지 간다, 대한민국!", ], }, { name: "eagle", lines: [ "남아공전 무조건 이긴다 🦅", "한국 16강 가즈아 🇰🇷", "남아공 골문을 노려라!", "이 경기 잡으면 토너먼트행 ✈️", "골! 골! 대한민국! ⚽", "아르헨도 한국 편 🇦🇷", "역습 한 방이면 끝난다", "오늘 클린시트 가자 🧤", "남아공 수비 흔들어!", "16강에서 또 보자 ✈️", ], }, { name: "rooster", lines: [ "꼬끼오~ 한국 남아공 이겨라! 🐓", "16강 길목, 오늘이 중요해", "남아공 막고 골 넣자!", "대한민국 파이팅 🇰🇷", "프랑스도 한국 편이다 🙌", "전반부터 몰아쳐라 🐓", "남아공 김빠지게 골 넣자", "한국 미드필드 최고지", "오늘 분위기 우리 거야", "꼬끼오! 승리의 함성 📣", ], }, { name: "parrot", lines: [ "삼바 응원! 한국 남아공 격파 🥁", "골 넣고 16강 가자 💃", "남아공 수비 흔들어! 🔥", "대한민국 가즈아 🇰🇷", "오늘 승점 3점 챙기자 ⚽", "리듬 타고 골 가자 🎶", "남아공? 삼바로 제압! 💃", "한 골 한 골 쌓아가자 ⚽", "응원석도 파도타기 🌊", "오늘 MVP는 대한민국 🏆", ], }, { name: "horse", lines: [ "달려라 대한민국, 남아공 잡자! 🏇", "16강 가는 길, 오늘이 분수령", "남아공 골문 열어라 ⚽", "끝까지 응원! 한국 화이팅 📣", "호날두도 한국 응원한다 🐴", "측면 돌파로 무너뜨려!", "스피드로 남아공 제쳐라 🏇", "한 점 차도 이기면 돼", "16강 티켓 가져오자 🎫", "포르투갈도 함께 외친다 🇵🇹", ], }, ]; const STEP_MS = 150; const REST = 1; const BUBBLE_MS = 2600; const TAP_THRESHOLD = 6; export default function WaveStrip() { const [active, setActive] = useState(-1); const [bubble, setBubble] = useState<{ i: number; text: string } | null>(null); const [off, setOff] = useState({ x: 0, y: 0 }); const [dragging, setDragging] = useState(false); const lastLine = useRef(""); const timer = useRef | undefined>(undefined); const drag = useRef({ px: 0, py: 0, ox: 0, oy: 0, moved: false, idx: -1 }); // 파도타기 루프 useEffect(() => { let tick = 0; const id = setInterval(() => { tick = (tick + 1) % (WAVE.length + REST); setActive(tick < WAVE.length ? tick : -1); }, STEP_MS); return () => clearInterval(id); }, []); useEffect(() => () => clearTimeout(timer.current), []); const speak = useCallback((i: number) => { const lines = WAVE[i].lines; let line = lines[Math.floor(Math.random() * lines.length)]; if (lines.length > 1) while (line === lastLine.current) line = lines[Math.floor(Math.random() * lines.length)]; lastLine.current = line; setBubble({ i, text: line }); clearTimeout(timer.current); timer.current = setTimeout(() => setBubble(null), BUBBLE_MS); }, []); // 그룹 전체 드래그 + 개별 탭 구분 const onPointerDown = useCallback( (e: React.PointerEvent) => { const el = (e.target as HTMLElement).closest("[data-idx]"); const idx = el ? Number((el as HTMLElement).dataset.idx) : -1; drag.current = { px: e.clientX, py: e.clientY, ox: off.x, oy: off.y, moved: false, idx }; setDragging(true); (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); }, [off], ); const onPointerMove = useCallback( (e: React.PointerEvent) => { if (!dragging) return; const dx = e.clientX - drag.current.px; const dy = e.clientY - drag.current.py; if (Math.abs(dx) > TAP_THRESHOLD || Math.abs(dy) > TAP_THRESHOLD) drag.current.moved = true; setOff({ x: drag.current.ox + dx, y: drag.current.oy + dy }); }, [dragging], ); const onPointerUp = useCallback(() => { setDragging(false); if (!drag.current.moved && drag.current.idx >= 0) speak(drag.current.idx); }, [speak]); return (
setDragging(false)} className="flex items-end justify-end gap-0" style={{ transform: `translate(${off.x}px, ${off.y}px) scale(${dragging ? 1.04 : 1})`, transition: dragging ? "none" : "transform 0.15s ease", cursor: dragging ? "grabbing" : "grab", touchAction: "none", position: "relative", zIndex: dragging || bubble ? 50 : 1, }} > {WAVE.map((a, i) => { const up = active === i; return (
{bubble?.i === i && (
{bubble.text}
)}
); })}
); }