From 6d57d2e3c2405d2ab641dc65cb5cecbfbc8f8fb5 Mon Sep 17 00:00:00 2001 From: cl-o-lc Date: Sun, 10 Aug 2025 23:59:19 +0900 Subject: [PATCH 1/9] =?UTF-8?q?style:=20=EB=B0=98=EC=9D=91=EC=86=8D?= =?UTF-8?q?=EB=8F=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20ui=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/reaction-test/page.tsx | 254 +++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 src/app/reaction-test/page.tsx diff --git a/src/app/reaction-test/page.tsx b/src/app/reaction-test/page.tsx new file mode 100644 index 0000000..c01736a --- /dev/null +++ b/src/app/reaction-test/page.tsx @@ -0,0 +1,254 @@ +'use client'; + +import { useEffect, useMemo, useRef, useState } from 'react'; + +type Phase = 'idle' | 'ready' | 'go' | 'tooSoon' | 'result'; +type LevelKey = 'easy' | 'normal' | 'hard'; + +const LEVELS: Record = { + easy: { label: '쉬움 (3–5초)', range: [3000, 5000] }, + normal: { label: '보통 (1–3초)', range: [1000, 3000] }, + hard: { label: '어려움 (0.5–2초)', range: [500, 2000] }, +}; + +export default function Page() { + const [level, setLevel] = useState('easy'); + const [phase, setPhase] = useState('idle'); + const [records, setRecords] = useState([]); + const [current, setCurrent] = useState(null); + const [best, setBest] = useState(null); + + const timerRef = useRef(null); + const startTsRef = useRef(null); + + // best 기록 로컬 저장 + useEffect(() => { + const saved = localStorage.getItem('reaction-best-ms'); + if (saved) setBest(Number(saved)); + }, []); + useEffect(() => { + if (best != null) localStorage.setItem('reaction-best-ms', String(best)); + }, [best]); + + // 평균 + const average = useMemo(() => { + if (records.length === 0) return null; + const sum = records.reduce((a, b) => a + b, 0); + return Math.round(sum / records.length); + }, [records]); + + const resetWaitingTimer = () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + }; + + const handleStart = () => { + if (phase === 'ready' || phase === 'go') return; + setCurrent(null); + setPhase('ready'); + const [min, max] = LEVELS[level].range; + const delay = Math.floor(Math.random() * (max - min + 1)) + min; + + resetWaitingTimer(); + timerRef.current = setTimeout(() => { + setPhase('go'); + startTsRef.current = performance.now(); + }, delay); + }; + + const handleCircleClick = () => { + if (phase === 'ready') { + // 너무 빨리 클릭 + resetWaitingTimer(); + setPhase('tooSoon'); + startTsRef.current = null; + return; + } + if (phase === 'go' && startTsRef.current != null) { + const rt = Math.round(performance.now() - startTsRef.current); + setCurrent(rt); + setRecords((prev) => [...prev, rt]); + setBest((prev) => (prev == null ? rt : Math.min(prev, rt))); + setPhase('result'); + startTsRef.current = null; + } + }; + + const handleReset = () => { + resetWaitingTimer(); + setPhase('idle'); + setCurrent(null); + setRecords([]); + }; + + const circleColor = + phase === 'go' + ? 'bg-red-500' + : phase === 'tooSoon' + ? 'bg-red-100' + : 'bg-gray-100'; + + const statusText = + phase === 'idle' + ? '게임 시작을 기다리는 중...' + : phase === 'ready' + ? '빨간색으로 바뀌면 바로 클릭하세요!' + : phase === 'go' + ? '지금! 클릭!' + : phase === 'tooSoon' + ? '너무 빨랐어요! (다시 시작)' + : current != null + ? `${current} ms` + : '결과 없음'; + + return ( +
+
+
+

반응속도 테스트

+

+ 티켓팅 성공을 위한 반응속도 훈련을 시작하세요! +

+
+ +
+ {/* 좌측: 메인 카드 */} +
+
+ {/* 난이도 탭 */} +
+ {(Object.keys(LEVELS) as LevelKey[]).map((k) => { + const active = level === k; + return ( + + ); + })} +
+ + {/* 원형 영역 */} +
+ + + {/* 안내 텍스트 */} +

+ 아래 버튼을 눌러 게임을 시작하세요 +

+ + {/* 버튼들 */} +
+ + +
+
+
+ + {/* 팁 카드 */} +
+

+ 🎯 반응속도 향상 팁 +

+
    +
  • + 🟡 화면을 집중해서 보고, 빨간색으로 바뀌는 순간 즉시 클릭! +
  • +
  • + 🟡 마우스나 터치패드를 편안하게 잡고 준비 자세를 취하세요. +
  • +
  • + 🟡 너무 일찍 클릭하지 마세요. 성급한 클릭은 실패로 처리됩니다. +
  • +
  • 🟡 꾸준한 연습이 평균 반응속도 향상의 핵심입니다.
  • +
+
+
+ + {/* 우측: 통계 카드 */} + +
+
+
+ ); +} From d455b8e05e2efa908dc78c35f10e555670514f9e Mon Sep 17 00:00:00 2001 From: cl-o-lc Date: Mon, 11 Aug 2025 00:00:01 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=B0=94=EC=97=90=20reaction-test=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/Header.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/ui/Header.tsx b/src/components/ui/Header.tsx index f569e5a..36489f4 100644 --- a/src/components/ui/Header.tsx +++ b/src/components/ui/Header.tsx @@ -55,7 +55,10 @@ export default function Header(props: HeaderProps) { 실시간 랭킹 - + 반응속도 게임 From 10e85277d6648caae3a7f6bc95e5f192e03df162 Mon Sep 17 00:00:00 2001 From: cl-o-lc Date: Mon, 11 Aug 2025 00:48:29 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=EB=8B=A8=EC=9D=BC=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C(0.5-5s)=EB=A1=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/reaction-test/page.tsx | 40 +++++----------------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/src/app/reaction-test/page.tsx b/src/app/reaction-test/page.tsx index c01736a..cbe20bf 100644 --- a/src/app/reaction-test/page.tsx +++ b/src/app/reaction-test/page.tsx @@ -3,16 +3,11 @@ import { useEffect, useMemo, useRef, useState } from 'react'; type Phase = 'idle' | 'ready' | 'go' | 'tooSoon' | 'result'; -type LevelKey = 'easy' | 'normal' | 'hard'; -const LEVELS: Record = { - easy: { label: '쉬움 (3–5초)', range: [3000, 5000] }, - normal: { label: '보통 (1–3초)', range: [1000, 3000] }, - hard: { label: '어려움 (0.5–2초)', range: [500, 2000] }, -}; +// 단일 모드: 0.5s ~ 5s +const DELAY_RANGE: [number, number] = [500, 5000]; export default function Page() { - const [level, setLevel] = useState('easy'); const [phase, setPhase] = useState('idle'); const [records, setRecords] = useState([]); const [current, setCurrent] = useState(null); @@ -21,7 +16,7 @@ export default function Page() { const timerRef = useRef(null); const startTsRef = useRef(null); - // best 기록 로컬 저장 + // 최고 기록 로컬 저장 useEffect(() => { const saved = localStorage.getItem('reaction-best-ms'); if (saved) setBest(Number(saved)); @@ -48,7 +43,8 @@ export default function Page() { if (phase === 'ready' || phase === 'go') return; setCurrent(null); setPhase('ready'); - const [min, max] = LEVELS[level].range; + + const [min, max] = DELAY_RANGE; const delay = Math.floor(Math.random() * (max - min + 1)) + min; resetWaitingTimer(); @@ -109,7 +105,7 @@ export default function Page() {

반응속도 테스트

- 티켓팅 성공을 위한 반응속도 훈련을 시작하세요! + 티켓팅 성공을 위한 반응속도 측정을 시작하세요!

@@ -117,30 +113,6 @@ export default function Page() { {/* 좌측: 메인 카드 */}
- {/* 난이도 탭 */} -
- {(Object.keys(LEVELS) as LevelKey[]).map((k) => { - const active = level === k; - return ( - - ); - })} -
- {/* 원형 영역 */}

- 이미 계정이 있으신가요?{' '} + 이미 계정이 있으신가요?