import React, { useRef, useCallback, useState, useEffect, forwardRef, useImperativeHandle } from 'react'; export interface InputProps extends React.InputHTMLAttributes { label?: string; error?: string; } /** * 기본 입력 필드 컴포넌트 * * IME(한글 등) 조합 중에는 외부 onChange 이벤트를 발생시키지 않습니다. * 내부 로컬 상태를 사용하여 입력을 처리하고, 조합이 완료되면 외부에 알립니다. * * forwardRef를 사용하여 외부에서 ref로 input 요소에 접근할 수 있습니다. * 예: indeterminate 상태 설정 시 ref.current.indeterminate = true */ export const Input = forwardRef(({ label, error, className = '', onChange, onKeyPress, value, defaultValue, type = 'text', checked, defaultChecked, ...props }, ref) => { // radio/checkbox 타입인지 확인 const isCheckableType = type === 'radio' || type === 'checkbox'; // IME 조합 중인지 추적 (text input에만 필요) const isComposingRef = useRef(false); // input 요소 참조 const internalRef = useRef(null); // 외부 ref와 내부 ref를 연결 useImperativeHandle(ref, () => internalRef.current as HTMLInputElement); // 내부 로컬 값 (IME 조합 중에도 화면에 표시됨) - text input에만 사용 const [localValue, setLocalValue] = useState( (value as string) ?? (defaultValue as string) ?? '' ); // 외부 value prop이 변경되면 로컬 값도 동기화 (text input에만 적용) useEffect(() => { if (!isCheckableType && !isComposingRef.current && value !== undefined) { setLocalValue(value as string); } }, [value, isCheckableType]); const handleCompositionStart = useCallback(() => { isComposingRef.current = true; }, []); const handleCompositionEnd = useCallback( (e: React.CompositionEvent) => { isComposingRef.current = false; // 조합 완료 후 onChange 이벤트 발생 if (onChange) { // 현재 input의 실제 값을 사용 const currentValue = (e.target as HTMLInputElement).value; setLocalValue(currentValue); // 새로운 ChangeEvent 생성 const changeEvent = new Event('change', { bubbles: true }) as unknown as React.ChangeEvent; Object.defineProperty(changeEvent, 'target', { writable: false, value: { value: currentValue }, }); Object.defineProperty(changeEvent, 'currentTarget', { writable: false, value: { value: currentValue }, }); onChange(changeEvent); } }, [onChange] ); const handleChange = useCallback( (e: React.ChangeEvent) => { const newValue = e.target.value; // 항상 로컬 값은 업데이트 (화면에 입력이 보이도록) setLocalValue(newValue); // IME 조합 중이면 외부 onChange를 호출하지 않음 if (isComposingRef.current) { return; } if (onChange) { onChange(e); } }, [onChange] ); const handleKeyPress = useCallback( (e: React.KeyboardEvent) => { // IME 조합 중이면 keypress 무시 (Enter 등) if (isComposingRef.current) { return; } if (onKeyPress) { onKeyPress(e); } }, [onKeyPress] ); // HTML 표준 속성이 아닌 것들을 필터링 (템플릿 엔진 내부 속성) const { loadingactions, formdata, ...validProps } = props as any; // radio/checkbox: checked 속성 사용 // text input: value 속성 사용 (IME 처리 포함) if (isCheckableType) { return ( ); } // text input: controlled/uncontrolled 구분 const inputProps = value !== undefined ? { value: localValue } : { defaultValue }; return ( ); }); // displayName 설정 (React DevTools에서 컴포넌트 이름 표시) Input.displayName = 'Input';