import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Sun, Moon, RotateCcw, Zap, Star, Gamepad2 } from 'lucide-react'; const shuffleArray = (array) => { const newArr = [...array]; for (let i = newArr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [newArr[i], newArr[j]] = [newArr[j], newArr[i]]; } return newArr; }; const playSound = (type) => { try { const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(audioCtx.destination); if (type === 'success') { osc.type = 'sine'; osc.frequency.setValueAtTime(600, audioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(1200, audioCtx.currentTime + 0.1); gain.gain.setValueAtTime(0.1, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2); osc.start(); osc.stop(audioCtx.currentTime + 0.2); } else if (type === 'error') { osc.type = 'square'; osc.frequency.setValueAtTime(150, audioCtx.currentTime); gain.gain.setValueAtTime(0.05, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0.01, audioCtx.currentTime + 0.1); osc.start(); osc.stop(audioCtx.currentTime + 0.1); } else if (type === 'complete') { [523, 659, 784, 1046].forEach((f, i) => { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.frequency.setValueAtTime(f, audioCtx.currentTime + i * 0.1); g.gain.setValueAtTime(0.1, audioCtx.currentTime + i * 0.1); g.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + i * 0.1 + 0.4); o.start(audioCtx.currentTime + i * 0.1); o.stop(audioCtx.currentTime + i * 0.1 + 0.4); }); } } catch (e) {} }; const App = () => { const [isDarkMode, setIsDarkMode] = useState(true); const [stage, setStage] = useState('intro'); const [msg, setMsg] = useState(''); const [subStage, setSubStage] = useState(0); const [scroll, setScroll] = useState(0); const [click, setClick] = useState(0); const [lastK, setLastK] = useState(null); const [hint, setHint] = useState(false); const [time, setTime] = useState(0); const [final, setFinal] = useState(0); const [active, setActive] = useState(null); const [monsterPos, setMonsterPos] = useState({ x: 50, y: 50 }); const [keys, setKeys] = useState([]); const [hangeul, setHangeul] = useState([]); const [combos, setCombos] = useState([]); const hintTimerRef = useRef(null); const monsterTimerRef = useRef(null); const layout = [ ['Esc', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'Del'], ['~', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 'Backspace'], ['Tab', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', '\\'], ['CapsLock', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', "'", 'Enter'], ['Shift', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 'Shift '], ['Ctrl', 'Win', 'Alt', 'Space', '한/영', 'Alt ', 'Ctrl '] ]; // 힌트 타이머 리셋 및 시작 로직 const resetHintTimer = useCallback(() => { setHint(false); if (hintTimerRef.current) { clearTimeout(hintTimerRef.current); } // 5초 후에 힌트를 보여줌 hintTimerRef.current = setTimeout(() => { setHint(true); }, 5000); }, []); // 타이머 즉시 중지 (잘못 입력했을 때 사용) const stopHintTimer = useCallback(() => { if (hintTimerRef.current) { clearTimeout(hintTimerRef.current); hintTimerRef.current = null; } }, []); const moveMonster = useCallback(() => { setMonsterPos({ x: Math.random() * 80 + 10, y: Math.random() * 60 + 20 }); }, []); const go = () => { setKeys(shuffleArray(['Escape', 'Tab', 'Delete', 'Backspace', 'Enter'])); setHangeul(shuffleArray([ { k: 'Q', l: 'ㅃ (쌍비읍)', t: 'Q' }, { k: 'W', l: 'ㅉ (쌍지읒)', t: 'W' }, { k: 'E', l: 'ㄸ (쌍디귿)', t: 'E' }, { k: 'R', l: 'ㄲ (쌍기역)', t: 'R' }, { k: 'T', l: 'ㅆ (쌍시옷)', t: 'T' } ])); setCombos(shuffleArray([ { n: '전체 선택 (Ctrl + A)', k: 'a', m: 'ctrl', t: 'A' }, { n: '복사하기 (Ctrl + C)', k: 'c', m: 'ctrl', t: 'C' }, { n: '붙여넣기 (Ctrl + V)', k: 'v', m: 'ctrl', t: 'V' } ])); setStage('scroll'); setMsg('마우스 휠을 아래로 굴려 길을 가세요!'); setScroll(0); setClick(0); setTime(Date.now()); playSound('success'); }; const onWheel = (e) => { if (stage === 'scroll' && e.deltaY > 0) { setScroll(s => { if (s + 1 >= 10) { setStage('click'); setMsg('움직이는 몬스터를 클릭해서 잡으세요!'); monsterTimerRef.current = setInterval(moveMonster, 1000); playSound('success'); return 10; } return s + 1; }); } }; const onClickMonster = (e) => { if (stage === 'click') { setClick(c => { if (c + 1 >= 10) { clearInterval(monsterTimerRef.current); setStage('rightClick'); setClick(0); setMsg('몬스터의 공격! 마우스 우클릭으로 방어막을 켜세요!'); playSound('complete'); return 10; } moveMonster(); playSound('success'); return c + 1; }); } }; const handleRightClick = (e) => { e.preventDefault(); if (stage === 'rightClick') { setClick(c => { if (c + 1 >= 5) { setStage('keys'); setSubStage(0); const first = keys[0]; const d = first === 'Escape' ? 'Esc' : (first === 'Delete' ? 'Del' : first); setMsg(`성문 열쇠 [${d}] 키를 찾으세요!`); setActive(d); // [중요] 키 찾기 문구가 처음 나올 때 타이머 작동 시작 resetHintTimer(); playSound('complete'); return 5; } playSound('success'); return c + 1; }); } }; const onKeyDown = useCallback((e) => { e.preventDefault(); const k = e.key; const upK = k.toUpperCase(); const displayKey = k === ' ' ? 'SPACE' : (k === 'Control' ? 'CTRL' : (k === 'Escape' ? 'ESC' : (k === 'Delete' ? 'DEL' : upK))); setLastK(displayKey); // 타겟이 있는 스테이지인지 확인 if (['keys', 'hangeul', 'combos'].includes(stage)) { let isCorrect = false; let isModifierOnly = ['Shift', 'Control', 'Alt', 'Meta'].includes(k); if (stage === 'keys') { const target = keys[subStage]; const isEsc = (target === 'Escape' || target === 'Esc') && (k === 'Escape' || k === 'Esc' || e.keyCode === 27); const isDel = (target === 'Delete' || target === 'Del') && (k === 'Delete' || k === 'Del' || e.keyCode === 46); const isOther = k.toLowerCase() === target.toLowerCase(); if (isEsc || isDel || isOther) isCorrect = true; } else if (stage === 'hangeul') { const target = hangeul[subStage]; if (e.shiftKey && k.toUpperCase() === target.k) isCorrect = true; } else if (stage === 'combos') { const target = combos[subStage]; const mod = target.m === 'ctrl' ? (e.ctrlKey || e.metaKey) : e.altKey; if (mod && k.toLowerCase() === target.k) isCorrect = true; } if (isCorrect) { playSound('success'); // 정답 시 로직 if (stage === 'keys') { if (subStage + 1 < keys.length) { const next = subStage + 1; setSubStage(next); const nextKey = keys[next]; const d = nextKey === 'Escape' ? 'Esc' : (nextKey === 'Delete' ? 'Del' : nextKey); setMsg(`다음 열쇠는 [${d}] 입니다!`); setActive(d); // [중요] 다음 열쇠 안내 시 타이머 다시 시작 resetHintTimer(); } else { setStage('hangeul'); setSubStage(0); setMsg(`마법 발동! Shift를 누른 채로 [${hangeul[0].l}]!`); setActive(hangeul[0].t); resetHintTimer(); } } else if (stage === 'hangeul') { if (subStage + 1 < hangeul.length) { const next = subStage + 1; setSubStage(next); setMsg(`마법 발동! Shift + [${hangeul[next].l}]!`); setActive(hangeul[next].t); resetHintTimer(); } else { setStage('combos'); setSubStage(0); setMsg(`필살기 준비! [${combos[0].n}]!`); setActive(combos[0].t); resetHintTimer(); } } else if (stage === 'combos') { if (subStage + 1 < combos.length) { const next = subStage + 1; setSubStage(next); setMsg(`필살기 준비! [${combos[next].n}]!`); setActive(combos[next].t); resetHintTimer(); } else { setFinal(((Date.now() - time) / 1000).toFixed(1)); setStage('end'); setMsg('와아! 네가 바로 전설의 키보드 용사야!'); setActive(null); stopHintTimer(); playSound('complete'); } } } else if (!isModifierOnly) { // [중요] 키를 잘못 입력하면 타이머 중지하고 즉시 힌트 보여주기 playSound('error'); stopHintTimer(); setHint(true); } } }, [stage, subStage, time, keys, hangeul, combos, resetHintTimer, stopHintTimer]); const onKeyUp = useCallback((e) => { e.preventDefault(); setLastK(null); }, []); useEffect(() => { window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp); return () => { window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); if (hintTimerRef.current) clearTimeout(hintTimerRef.current); if (monsterTimerRef.current) clearInterval(monsterTimerRef.current); }; }, [onKeyDown, onKeyUp]); const drawKey = (label) => { const clean = label.toUpperCase().trim(); const norm = label.toLowerCase().trim(); const act = active ? active.toLowerCase().trim() : ''; const isTarget = active && ( norm === act || (act === 'esc' && norm === 'escape') || (act === 'del' && norm === 'delete') ); const isPressed = lastK === clean || (clean === 'ESC' && lastK === 'ESCAPE') || (clean === 'DEL' && lastK === 'DELETE'); let cls = `flex items-center justify-center rounded-lg border-2 text-[11px] font-black transition-all duration-75 select-none `; if (isTarget && hint) { // 힌트 상태: 노란색 배경에 검은색 글씨, 강한 그림자 효과 cls += "bg-yellow-400 text-black border-yellow-200 scale-110 animate-bounce z-10 shadow-[0_0_20px_rgba(250,204,21,1)] "; } else if (isPressed) { cls += "bg-orange-500 text-white border-orange-300 scale-90 "; } else { cls += isDarkMode ? "bg-slate-800 text-slate-400 border-slate-700 " : "bg-white text-slate-400 border-slate-200 "; } let s = { minWidth: '38px', height: '38px' }; if (['Backspace', 'Enter', 'Shift ', 'Tab', 'CapsLock', 'Ctrl', 'Shift', 'Space', '한/영', 'Del'].includes(label)) { s.minWidth = label === 'Space' ? '180px' : '60px'; } return
{formatMsg(msg)}
{formatMsg(msg)}
{formatMsg(msg)}
{final}초 만에 평화를 되찾았어!