/* ============================================================================ * 서명전에 — TDS TextField 컴포넌트 (Task #167) * * · 텍스트 입력의 표준 진입점. label / placeholder / help 한 묶음을 관리. * · 4 가지 변종(variant): box · line · big · hero * · 라벨 표시 옵션(labelOption): "appear" (값/포커스 시에만) · "sustain" (항상) * · prefix / suffix : 텍스트 addon (예: "원", "@gmail.com") * · right : 임의 ReactNode 슬롯 (Clearable / Password / Button 이 자체적으로 채움) * · hasError + handleError : 에러 상태 + 에러 문구 * · disabled : 입력 차단 + 시각적 회색 처리 * · format.transform / format.reset : 입력값 포매팅 함수 (예: "1000000" → "1,000,000") * · paddingTop / paddingBottom : 외곽 여백 커스텀 * · controlled (value+onChange) / uncontrolled (defaultValue) 모두 지원 * * · 서브 컴포넌트: * TextField.Clearable — 우측 X 버튼 자동 (값 비어있을 땐 숨김) * TextField.Password — type="password" + 눈 토글 버튼 * TextField.Button — input 대신 button 처럼 동작 (피커 진입용 — 예: 날짜 선택) * * · 디자인 토큰: * box 높이 52 / 둥글기 14 / 회색100 배경 / 포커스 시 ring * line 하단 1.5px 라인 / 포커스 시 brand 라인 * big 높이 64 / 라벨 굵게 / input ty-4 굵게 * hero 박스 없음 / input ty-2 bold + tnum (금액 입력) * * · Babel Standalone 환경 호환: * React.* 훅을 직접 호출하지 않고 __tfUseState 등 alias 로 호출. * (다른 스크립트의 동일 이름 변수와 충돌 회피) * ========================================================================== */ // ----- React 훅 alias (Babel Standalone 충돌 방지) ----- const __tfUseState = (typeof React !== "undefined" && React.useState) || function (v) { return [v, function () {}]; }; const __tfUseRef = (typeof React !== "undefined" && React.useRef) || function () { return { current: null }; }; const __tfUseEffect = (typeof React !== "undefined" && React.useEffect) || function () {}; const __tfUseCallback = (typeof React !== "undefined" && React.useCallback) || function (fn) { return fn; }; const __tfUseMemo = (typeof React !== "undefined" && React.useMemo) || function (fn) { return fn(); }; const __tfForwardRef = (typeof React !== "undefined" && React.forwardRef) || function (fn) { return fn; }; const __tfUseImperativeHandle= (typeof React !== "undefined" && React.useImperativeHandle) || function () {}; // ----- 클래스 조립 헬퍼 ----- function __tfJoin() { let out = ""; for (let i = 0; i < arguments.length; i++) { const v = arguments[i]; if (!v) continue; out = out ? out + " " + v : v; } return out; } // ----- enum 가드 ----- function __tfEnum(value, allowed, fallback, propPath) { if (allowed.indexOf(value) !== -1) return value; if (value !== undefined && window.console && console.warn) { console.warn("[TextField] " + propPath + " 값이 잘못됨: " + JSON.stringify(value) + ". 허용: " + allowed.join(", ") + ". 기본 " + fallback + " 사용."); } return fallback; } // ----- X SVG ----- function __tfClearSvg() { return ( ); } // ----- 눈(보임) SVG ----- function __tfEyeOpenSvg() { return ( ); } // ----- 눈(가림) SVG ----- function __tfEyeOffSvg() { return ( ); } // ----- 화살표 SVG (TextField.Button 의 기본 chevron) ----- function __tfChevronSvg() { return ( ); } /* ---------------------------------------------------------------------------- * TextField (root) — forwardRef * ref API: * focus() — input 으로 포커스 * blur() — 포커스 해제 * clear() — 값 비우기 + onChange 호출 * el() — 내부 input DOM 반환 * -------------------------------------------------------------------------- */ const TextField = __tfForwardRef(function TextField(props, ref) { const { // 기본 props label, placeholder = "", help, variant: variantProp, labelOption: labelOptionProp, hasError = false, handleError, disabled = false, required = false, // 슬롯 prefix, suffix, right, // 값 / 콜백 value, defaultValue, onChange, onFocus, onBlur, // 포매팅 format, // 입력 type — 기본 "text" type = "text", // 외곽 여백 paddingTop, paddingBottom, // 외부에서 변종 강제 (서브 컴포넌트가 사용) __subRight, // 패스스루 className = "", style, id, name, autoFocus, autoComplete, inputMode, maxLength, "aria-label": ariaLabel, ...rest } = props; const variant = __tfEnum(variantProp, ["box", "line", "big", "hero"], "box", "variant"); const labelOption = __tfEnum(labelOptionProp, ["appear", "sustain"], "sustain", "labelOption"); // ----- controlled / uncontrolled ----- const isControlled = value !== undefined; const [internalValue, setInternalValue] = __tfUseState(typeof defaultValue === "string" ? defaultValue : ""); const rawValue = isControlled ? (value == null ? "" : String(value)) : internalValue; // ----- 포매팅된 표시값 ----- const displayValue = __tfUseMemo(function () { if (format && typeof format.transform === "function") { try { return format.transform(rawValue); } catch (_e) { return rawValue; } } return rawValue; }, [rawValue, format]); // ----- 포커스 상태 ----- const [focused, setFocused] = __tfUseState(false); // ----- ref ----- const inputRef = __tfUseRef(null); __tfUseImperativeHandle(ref, function () { return { focus: function () { if (inputRef.current && inputRef.current.focus) inputRef.current.focus(); }, blur: function () { if (inputRef.current && inputRef.current.blur) inputRef.current.blur(); }, clear: function () { __tfApplyChange(""); }, el: function () { return inputRef.current; }, }; }, [isControlled]); // ----- 값 적용 헬퍼 ----- function __tfApplyChange(nextRaw) { let nv = nextRaw; // format.reset 이 있으면 표시값 → 원시값 복원 if (format && typeof format.reset === "function") { try { nv = format.reset(nextRaw); } catch (_e) { nv = nextRaw; } } if (!isControlled) setInternalValue(nv); if (typeof onChange === "function") { const synthetic = { target: { value: nv, name: name }, currentTarget: inputRef.current, type: "change", }; onChange(synthetic); } } // ----- 핸들러 ----- const handleInputChange = function (e) { __tfApplyChange(e.target.value); }; const handleFocus = function (e) { setFocused(true); if (typeof onFocus === "function") onFocus(e); }; const handleBlur = function (e) { setFocused(false); if (typeof onBlur === "function") onBlur(e); }; // ----- label visibility (appear 모드) ----- const hasValueOrFocus = focused || (rawValue && rawValue.length > 0); const labelShown = labelOption === "sustain" || (labelOption === "appear" && hasValueOrFocus); // ----- 클래스 ----- const rootCls = __tfJoin( "tds-tf", "tds-tf--" + variant, "tds-tf--" + labelOption, labelShown ? "tds-tf--shown" : "", focused ? "tds-tf--focused" : "", hasError ? "tds-tf--error" : "", disabled ? "tds-tf--disabled" : "", className ); // ----- 스타일 (paddingTop/Bottom) ----- const finalStyle = Object.assign({}, style || {}); if (paddingTop != null) finalStyle.paddingTop = typeof paddingTop === "number" ? paddingTop + "px" : paddingTop; if (paddingBottom != null) finalStyle.paddingBottom = typeof paddingBottom === "number" ? paddingBottom + "px" : paddingBottom; // ----- 본체 ----- return (
{hasError && handleError ? handleError : help}
) : null}{hasError && handleError ? handleError : help}
) : null}