"use client"; import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useRef, useState, } from "react"; import type { FocusEvent, FormEvent } from "react"; import { DESKTOP_INPUT_DEVICE_QUERY } from "@/lib/input-device"; import KatexRenderer from "@/components/math/KatexRenderer"; export type ActiveAnswerInput = number; const ANSWER_INPUT_PATTERN = /^-?\d*(?:\.\d*)?$/; const KEYPAD_KEYS = [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "+/-", "0", ]; export interface NumberAnswerFormHandle { focusInput: () => void; } interface NumberAnswerFormProps { answers: string[]; activeInput: ActiveAnswerInput; error: string; pendingAnswerLabels: string[]; onAnswerChange: (index: ActiveAnswerInput, value: string) => void; onActiveInputChange: (input: ActiveAnswerInput) => void; onInputBlur?: () => void; onInputFocus?: () => void; onSubmit: (event: FormEvent) => void; } const NumberAnswerForm = forwardRef< NumberAnswerFormHandle, NumberAnswerFormProps >(function NumberAnswerForm( { answers, activeInput, error, pendingAnswerLabels, onAnswerChange, onActiveInputChange, onInputBlur, onInputFocus, onSubmit, }, ref, ) { const inputId = useId(); const formRef = useRef(null); const inputRef = useRef(null); const blurTimeoutRef = useRef(null); const [keypadOpen, setKeypadOpen] = useState(false); const [desktopInputDevice, setDesktopInputDevice] = useState(false); const activeAnswer = answers[activeInput] ?? ""; const activeLabel = `${activeInput + 1}항`; const activeDescription = `${activeLabel}의 답을 입력하세요.`; const lastInputIndex = answers.length - 1; const activeEnterKeyHint = activeInput < lastInputIndex ? "next" : "done"; function updateAnswer(value: string) { if (!ANSWER_INPUT_PATTERN.test(value)) return; onAnswerChange(activeInput, value); } function handleInputFocus() { if (blurTimeoutRef.current !== null) { window.clearTimeout(blurTimeoutRef.current); blurTimeoutRef.current = null; } setKeypadOpen(true); onInputFocus?.(); } function handleInputBlur(event: FocusEvent) { const nextFocusedElement = event.relatedTarget; if ( nextFocusedElement instanceof Node && formRef.current?.contains(nextFocusedElement) ) { return; } blurTimeoutRef.current = window.setTimeout(() => { blurTimeoutRef.current = null; if (formRef.current?.contains(document.activeElement)) { setKeypadOpen(true); return; } setKeypadOpen(false); onInputBlur?.(); }, 0); } const focusInput = useCallback(() => { const input = inputRef.current; if (!input) return; const wasFocused = document.activeElement === input; input.focus({ preventScroll: true }); if (wasFocused) { onInputFocus?.(); } }, [onInputFocus]); useImperativeHandle(ref, () => ({ focusInput }), [focusInput]); useEffect(() => { const mediaQuery = window.matchMedia(DESKTOP_INPUT_DEVICE_QUERY); const updateInputDevice = () => { setDesktopInputDevice(mediaQuery.matches); }; updateInputDevice(); mediaQuery.addEventListener("change", updateInputDevice); return () => { if (blurTimeoutRef.current !== null) { window.clearTimeout(blurTimeoutRef.current); } mediaQuery.removeEventListener("change", updateInputDevice); }; }, []); function toggleMinus() { onAnswerChange( activeInput, activeAnswer.startsWith("-") ? activeAnswer.slice(1) : `-${activeAnswer}`, ); focusInput(); } function appendKeypadValue(value: string) { updateAnswer(`${activeAnswer}${value}`); focusInput(); } function deleteLastCharacter() { updateAnswer(activeAnswer.slice(0, -1)); focusInput(); } function moveToInput(input: ActiveAnswerInput, reset = false) { onActiveInputChange(input); if (reset) { onAnswerChange(input, ""); } focusInput(); } function handleEnter() { if (activeInput < lastInputIndex) { moveToInput(activeInput + 1); return; } formRef.current?.requestSubmit(); focusInput(); } const answerButtons = answers.map((value, index) => ({ index, label: `${index + 1}항`, value, })); return (
    {answerButtons.map((button) => { const selected = activeInput === button.index; const completed = button.value !== ""; const displayValue = button.value || pendingAnswerLabels[button.index]; return (
  1. ); })}
updateAnswer(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); handleEnter(); } }} autoComplete="off" enterKeyHint={activeEnterKeyHint} inputMode="none" pattern="-?[0-9]*(\.[0-9]*)?" className="h-12 w-full min-w-0 rounded-md border border-slate-300 bg-white px-3 text-lg font-semibold text-slate-950 outline-none transition focus:border-emerald-600 focus:ring-2 focus:ring-emerald-200 sm:px-4" /> {keypadOpen && !desktopInputDevice ? (
{KEYPAD_KEYS.map((key) => ( ))}
) : null}
{error ? (

{error}

) : null}
); }); export default NumberAnswerForm;