/** 명령행 — AutoCAD 명령행처럼 기록·입력·자동완성을 담당한다. */ import { type FC, type FormEvent, useMemo, useRef, useState } from 'react'; import { describeCommand, matchCommandPrefixes } from '../commands/registry'; import { getCommandHistory, runCommandInput } from '../commands/run-command'; import { activeToolWantsFreeText, isPlainSpace } from '../inputController/ime-sink'; import { commandCandidate, recallInput } from '../inputController/input-history'; import { currentPrompt } from '../inputController/prompt-input'; import { getInputController } from '../state'; export const CommandLine: FC = () => { const [text, setText] = useState(''); /** Tab · ↑↓ 후보 돌림 — 처음 친 글 · 몇째(null = 안 돌림) */ const [pick, setPick] = useState<{ base: string; index: number } | null>(null); const [fromHistory, setFromHistory] = useState(false); const inputRef = useRef(null); const history = getCommandHistory(); const instruction = currentPrompt().line || '명령을 입력하거나 도구를 선택하십시오.'; const suggestions = useMemo(() => matchCommandPrefixes(pick?.base ?? text).slice(0, 8), [text, pick]); const submit = (event: FormEvent) => { event.preventDefault(); const value = text.trim(); if (!value) { // 빈 ENTER는 활성 명령의 확정 신호다 (선택 끝내기·직전 명령 반복) getInputController().handleEnterKey(true); return; } // 진행 중인 명령이 문자·숫자를 기다리면 그쪽으로, 아니면 명령으로 해석된다 getInputController().submitText(value); setText(''); setPick(null); setFromHistory(false); }; /** 후보 보이면 후보 돌림 · 아니면 친 명령 · 최근 값 기록(캔버스와 같은 함수 · 알림 줄 안 섞음) */ const recall = (direction: -1 | 1) => { if (pick || (!fromHistory && text.trim() && suggestions.length)) { cycle(direction); return; } const value = recallInput(direction, text); if (value === null) return; setText(value); setFromHistory(true); }; const cycle = (direction: -1 | 1) => { const base = pick?.base ?? text; const index = pick ? pick.index + direction : direction > 0 ? 0 : -1; const name = commandCandidate(base, index); if (!name) return; setPick({ base, index }); setText(name); }; return (
{history.slice(-3).map((line, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: 기록은 순서 자체가 식별자다 {line} ))}
{ setText(event.target.value); setPick(null); setFromHistory(false); }} onKeyDown={(event) => { if (event.nativeEvent.isComposing) return; // 한글 조합 중 키는 IME 몫 if (event.key === ' ' && !activeToolWantsFreeText()) { // Space = Enter(오토캐드) · 글 단계는 띄어쓰기 · 수정키 Space 는 그대로(입력칸 몫) if (isPlainSpace(event)) { event.preventDefault(); event.currentTarget.form?.requestSubmit(); } else if (event.repeat) event.preventDefault(); return; } if (event.key === 'Escape') { event.preventDefault(); getInputController().handleEscapeKey(); } if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { event.preventDefault(); recall(event.key === 'ArrowUp' ? -1 : 1); } if (event.key === 'Tab' && suggestions.length) { event.preventDefault(); cycle(1); } }} placeholder="명령 입력 (예: L, LINE, REC, MOVE)" autoComplete="off" spellCheck={false} /> {instruction}
{text.trim() && suggestions.length > 0 && (
{suggestions.map((command) => ( ))}
)}
); };