맥락: - /space에서 실수 이탈을 줄이면서도 명확한 탈출 동선을 유지하기 위해 나가기 액션을 1초 롱프레스 방식으로 변경 변경사항: - features/exit-hold 추가(useHoldToConfirm, ExitHoldButton) - 1초 롱프레스와 가속 진행 규칙(0.05초 -> 20%)을 feature 내부에 구현 - 몰입 OFF에서는 bar(좌->우 fill), 몰입 ON에서는 ring 진행 표시로 분기 - 1초 미만 해제/마우스 leave/touch cancel 시 진행률 즉시 리셋 - 완료 시 나가기(더미) 토스트 + 몰입 모드 OFF 동작 연결 - docs/90_current_state.md, docs/session_brief.md 상태 업데이트 검증: - npx tsc --noEmit 세션-상태: 상단 나가기 액션은 롱프레스 완료 시에만 트리거됨 세션-다음: 롱프레스 인터랙션 인지성 보완용 힌트 카피 도입 여부 검토 세션-리스크: 터치 환경에서 롱프레스 UI 의도를 즉시 이해하지 못할 가능성 있음
92 lines
2.0 KiB
TypeScript
92 lines
2.0 KiB
TypeScript
'use client';
|
|
|
|
import { useRef, useState } from 'react';
|
|
|
|
const HOLD_DURATION_MS = 1000;
|
|
const BOOST_DURATION_MS = 50;
|
|
|
|
const mapProgress = (elapsedMs: number) => {
|
|
if (elapsedMs <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (elapsedMs <= BOOST_DURATION_MS) {
|
|
return 0.2 * (elapsedMs / BOOST_DURATION_MS);
|
|
}
|
|
|
|
const tailElapsedMs = Math.min(elapsedMs - BOOST_DURATION_MS, HOLD_DURATION_MS - BOOST_DURATION_MS);
|
|
return 0.2 + 0.8 * (tailElapsedMs / (HOLD_DURATION_MS - BOOST_DURATION_MS));
|
|
};
|
|
|
|
export const useHoldToConfirm = (onConfirm: () => void) => {
|
|
const frameRef = useRef<number | null>(null);
|
|
const startRef = useRef<number | null>(null);
|
|
const confirmedRef = useRef(false);
|
|
const [progress, setProgress] = useState(0);
|
|
const [isHolding, setHolding] = useState(false);
|
|
|
|
const clearFrame = () => {
|
|
if (frameRef.current !== null) {
|
|
window.cancelAnimationFrame(frameRef.current);
|
|
frameRef.current = null;
|
|
}
|
|
};
|
|
|
|
const reset = () => {
|
|
clearFrame();
|
|
startRef.current = null;
|
|
confirmedRef.current = false;
|
|
setHolding(false);
|
|
setProgress(0);
|
|
};
|
|
|
|
const step = () => {
|
|
if (startRef.current === null) {
|
|
return;
|
|
}
|
|
|
|
const elapsedMs = performance.now() - startRef.current;
|
|
const nextProgress = mapProgress(elapsedMs);
|
|
const clampedProgress = Math.min(nextProgress, 1);
|
|
setProgress(clampedProgress);
|
|
|
|
if (clampedProgress >= 1 && !confirmedRef.current) {
|
|
confirmedRef.current = true;
|
|
onConfirm();
|
|
window.setTimeout(() => {
|
|
reset();
|
|
}, 120);
|
|
return;
|
|
}
|
|
|
|
frameRef.current = window.requestAnimationFrame(step);
|
|
};
|
|
|
|
const start = () => {
|
|
if (isHolding) {
|
|
return;
|
|
}
|
|
|
|
clearFrame();
|
|
confirmedRef.current = false;
|
|
startRef.current = performance.now();
|
|
setHolding(true);
|
|
frameRef.current = window.requestAnimationFrame(step);
|
|
};
|
|
|
|
const cancel = () => {
|
|
if (!isHolding) {
|
|
return;
|
|
}
|
|
|
|
reset();
|
|
};
|
|
|
|
return {
|
|
progress,
|
|
isHolding,
|
|
start,
|
|
cancel,
|
|
};
|
|
};
|