Files
jaengseung-made/app/components/deepfield/CountUp.tsx
gahusb 4cd4a50869 feat(home): Deep Field 다크 캔버스 재조립 + 운영 실증 카피
- HERO/SHOWCASE/PROCESS/PROOF/SOFTWARE+CTA 5섹션 다크(--jsm-dark-bg) 재구성
- HeroField WebGL 배경 + -mt-16/pt-16로 상단 라이트 띠 제거 (PublicShell 무수정)
- "생각을 동작하는 소프트웨어로." 거대 타이포(clamp, -0.04em)
- 경력·소속 표현 전면 제거 → "24시간 돌아가는 실서비스 직접 설계·운영" 신뢰 축
- CountUp 카운트업 스탯 + 스크롤 큐 keyframes(motion-safe 가드)
- layout metadata·jsonLd 카피 동일 톤 교체 (jobTitle "소프트웨어 엔지니어")

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 00:57:38 +09:00

77 lines
2.1 KiB
TypeScript

'use client';
import { useEffect, useRef, useState } from 'react';
interface Props {
/** 카운트업 목표 숫자 */
to: number;
/** 숫자 앞에 붙는 고정 텍스트 (예: 없음) */
prefix?: string;
/** 숫자 뒤에 붙는 고정 텍스트 (예: '+') */
suffix?: string;
/** 애니메이션 길이(ms) — 기본 600 */
duration?: number;
className?: string;
}
/**
* IntersectionObserver 진입 시 0 → to 로 카운트업.
* prefers-reduced-motion이면 즉시 최종값 표시(연출 생략).
* transform/opacity가 아닌 textContent 변경이라 레이아웃 안정 위해 tabular-nums 권장.
*/
export default function CountUp({ to, prefix = '', suffix = '', duration = 600, className }: Props) {
const ref = useRef<HTMLSpanElement>(null);
const [value, setValue] = useState(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
let rafId = 0;
let started = false;
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const run = () => {
// reduced-motion: 즉시 최종값 (연출 생략)
if (reduced) {
setValue(to);
return;
}
const start = performance.now();
const tick = (now: number) => {
const t = Math.min((now - start) / duration, 1);
// easeOutCubic — 끝에서 부드럽게 안착
const eased = 1 - Math.pow(1 - t, 3);
setValue(Math.round(eased * to));
if (t < 1) rafId = requestAnimationFrame(tick);
};
rafId = requestAnimationFrame(tick);
};
const io = new IntersectionObserver(
(entries) => {
if (entries[0]?.isIntersecting && !started) {
started = true;
run();
io.disconnect();
}
},
{ threshold: 0.4 },
);
io.observe(el);
return () => {
io.disconnect();
if (rafId) cancelAnimationFrame(rafId);
};
}, [to, duration]);
return (
<span ref={ref} className={className} style={{ fontVariantNumeric: 'tabular-nums' }}>
{prefix}
{value.toLocaleString('ko-KR')}
{suffix}
</span>
);
}