"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 = 0 | 1; 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 { answerA: string; answerB: string; activeInput: ActiveAnswerInput; error: string; pendingAnswerLabels: [string, string]; onAnswerAChange: (value: string) => void; onAnswerBChange: (value: string) => void; onActiveInputChange: (input: ActiveAnswerInput) => void; onInputBlur?: () => void; onInputFocus?: () => void; onSubmit: (event: FormEvent) => void; } const NumberAnswerForm = forwardRef< NumberAnswerFormHandle, NumberAnswerFormProps >(function NumberAnswerForm( { answerA, answerB, activeInput, error, pendingAnswerLabels, onAnswerAChange, onAnswerBChange, 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 = activeInput === 0 ? answerA : answerB; const activeLabel = activeInput === 0 ? "첫째 항" : "둘째 항"; const activeDescription = `${activeLabel}의 답을 입력하세요.`; const activeEnterKeyHint = activeInput === 0 ? "next" : "done"; const activeAnswerChange = activeInput === 0 ? onAnswerAChange : onAnswerBChange; function updateAnswer(value: string) { if (!ANSWER_INPUT_PATTERN.test(value)) return; activeAnswerChange(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 setAnswer(input: ActiveAnswerInput, value: string) { if (input === 0) { onAnswerAChange(value); } else { onAnswerBChange(value); } } function toggleMinus() { activeAnswerChange( 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) { setAnswer(input, ""); } focusInput(); } function handleEnter() { if (activeInput === 0) { moveToInput(1); return; } formRef.current?.requestSubmit(); focusInput(); } const answerButtons = [ { index: 0 as const, label: "첫째 항", value: answerA, }, { index: 1 as const, label: "둘째 항", value: answerB, }, ]; 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;