@ehfuse/mui-form-controls 3.0.23 → 3.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/address.ts", "../src/AddressTextField.tsx", "../src/hooks/useTextFieldBase.ts", "../src/hooks/useKoreanHolidays.ts", "../src/hooks/useGroupedInput.ts"],
4
- "sourcesContent": ["export { AddressTextField } from \"./AddressTextField\";\nexport type { AddressTextFieldProps, DaumPostcodeData } from \"./types\";\n", "/**\r\n * AddressTextField.tsx\r\n *\r\n * @license MIT\r\n form,\r\n * @copyright 2025 \uAE40\uC601\uC9C4 (Kim Young Jin)\r\n * @author \uAE40\uC601\uC9C4 (ehfuse@gmail.com)\r\n */\r\n\r\nimport {\r\n IconButton,\r\n InputAdornment,\r\n TextField,\r\n Dialog,\r\n DialogContent,\r\n DialogTitle,\r\n} from \"@mui/material\";\r\nimport SearchIcon from \"@mui/icons-material/Search\";\r\nimport CloseIcon from \"@mui/icons-material/Close\";\r\nimport {\r\n useState,\r\n useEffect,\r\n type ChangeEvent,\r\n useCallback,\r\n forwardRef,\r\n useRef,\r\n useImperativeHandle,\r\n} from \"react\";\r\nimport DaumPostcode from \"react-daum-postcode\";\r\nimport type { AddressTextFieldProps, DaumPostcodeData } from \"./types\";\r\nimport { useDebouncedEmit } from \"./hooks\";\r\n\r\nconst AddressTextFieldBase = forwardRef<HTMLDivElement, AddressTextFieldProps>(\r\n function AddressTextFieldBase(\r\n {\r\n value,\r\n onChange,\r\n readonly,\r\n debounce,\r\n onBlur,\r\n size,\r\n spellCheck = false,\r\n inputRef: externalInputRef,\r\n ...props\r\n },\r\n ref,\r\n ) {\r\n const internalInputRef = useRef<HTMLInputElement>(null);\r\n\r\n // \uC678\uBD80 inputRef\uC640 \uB0B4\uBD80 inputRef \uBCD1\uD569\r\n useImperativeHandle(\r\n externalInputRef as React.Ref<HTMLInputElement>,\r\n () => internalInputRef.current!,\r\n [],\r\n );\r\n\r\n const [address, setAddress] = useState<string>(\r\n typeof value === \"string\" ? value : \"\",\r\n );\r\n const [isPostcodeOpen, setIsPostcodeOpen] = useState(false);\r\n\r\n // size\uC5D0 \uB530\uB978 \uC544\uC774\uCF58 \uD06C\uAE30\r\n const iconSize = size === \"small\" ? 20 : 24;\r\n\r\n // useDebouncedEmit \uD6C5 \uC0AC\uC6A9\r\n const { emitChange, flushOnBlur, lastEmittedValue } = useDebouncedEmit({\r\n name: props.name,\r\n debounce,\r\n onChange,\r\n });\r\n\r\n // value prop\uC774 \uBCC0\uACBD\uB420 \uB54C \uB0B4\uBD80 state \uC5C5\uB370\uC774\uD2B8 (\uC678\uBD80\uC5D0\uC11C \uBCC0\uACBD\uB41C \uACBD\uC6B0\uB9CC)\r\n useEffect(() => {\r\n if (value !== undefined && value !== null) {\r\n const strValue = String(value);\r\n // \uC678\uBD80\uC5D0\uC11C \uBCC0\uACBD\uB41C \uAC12\uC774 \uB9C8\uC9C0\uB9C9\uC73C\uB85C emit\uD55C \uAC12\uACFC \uB2E4\uB97C \uB54C\uB9CC \uB3D9\uAE30\uD654\r\n if (strValue !== lastEmittedValue.current) {\r\n setAddress(strValue);\r\n lastEmittedValue.current = strValue;\r\n }\r\n } else if (lastEmittedValue.current !== \"\") {\r\n setAddress(\"\");\r\n lastEmittedValue.current = \"\";\r\n }\r\n }, [value, lastEmittedValue]);\r\n\r\n const handlePostcodeComplete = (data: DaumPostcodeData) => {\r\n let fullAddress = data.address;\r\n let extraAddress = \"\";\r\n\r\n // \uC0AC\uC6A9\uC790\uAC00 \uC120\uD0DD\uD55C \uC8FC\uC18C \uD0C0\uC785\uC5D0 \uB530\uB77C \uD574\uB2F9 \uC8FC\uC18C \uAC12\uC744 \uAC00\uC838\uC628\uB2E4.\r\n if (data.addressType === \"R\") {\r\n // \uB3C4\uB85C\uBA85 \uC8FC\uC18C\uB97C \uC120\uD0DD\uD588\uC744 \uACBD\uC6B0\r\n if (data.bname !== \"\" && /[\uB3D9|\uB85C|\uAC00]$/g.test(data.bname)) {\r\n extraAddress += data.bname;\r\n }\r\n if (data.buildingName !== \"\" && data.apartment === \"Y\") {\r\n extraAddress +=\r\n extraAddress !== \"\"\r\n ? \", \" + data.buildingName\r\n : data.buildingName;\r\n }\r\n if (extraAddress !== \"\") {\r\n extraAddress = \" (\" + extraAddress + \")\";\r\n }\r\n fullAddress += extraAddress;\r\n }\r\n\r\n // \uC8FC\uC18C \uC124\uC815\r\n setAddress(fullAddress);\r\n\r\n // \uC8FC\uC18C \uBCC0\uACBD \uC774\uBCA4\uD2B8 \uBC1C\uC0DD (\uD31D\uC5C5 \uC120\uD0DD\uC740 \uC989\uC2DC \uD638\uCD9C)\r\n emitChange(null, fullAddress, true);\r\n\r\n // \uD31D\uC5C5 \uB2EB\uAE30\r\n setIsPostcodeOpen(false);\r\n };\r\n\r\n const handleSearchButtonClick = () => {\r\n setIsPostcodeOpen(true);\r\n };\r\n\r\n const handleKeyDown = (\r\n event: React.KeyboardEvent<HTMLInputElement>,\r\n ) => {\r\n if (event.key === \"Enter\") {\r\n event.preventDefault();\r\n handleSearchButtonClick();\r\n }\r\n };\r\n\r\n const handlePostcodeClose = () => {\r\n setIsPostcodeOpen(false);\r\n };\r\n\r\n const onClose = () => {\r\n setIsPostcodeOpen(false);\r\n emitChange(null, address as string, true);\r\n };\r\n\r\n // blur \uC2DC \uB300\uAE30 \uC911\uC778 \uB514\uBC14\uC6B4\uC2A4 \uC989\uC2DC \uC2E4\uD589\r\n const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {\r\n flushOnBlur(e, address as string);\r\n if (onBlur) {\r\n onBlur(e);\r\n }\r\n };\r\n\r\n // readonly\uC77C \uB54C \uD3EC\uCEE4\uC2A4 \uC2A4\uD0C0\uC77C \uC81C\uAC70\r\n const readonlyStyles = readonly\r\n ? {\r\n \"& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline\":\r\n {\r\n borderColor: \"rgba(0, 0, 0, 0.23)\",\r\n borderWidth: 1,\r\n },\r\n \"& .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline\":\r\n {\r\n borderColor: \"rgba(0, 0, 0, 0.23)\",\r\n },\r\n \"& .MuiInputLabel-root.Mui-focused\": {\r\n color: \"rgba(0, 0, 0, 0.6)\",\r\n },\r\n }\r\n : {};\r\n\r\n return (\r\n <>\r\n <TextField\r\n {...props}\r\n ref={ref}\r\n inputRef={internalInputRef}\r\n size={size}\r\n value={address}\r\n onChange={\r\n readonly\r\n ? undefined\r\n : (e) => {\r\n const newValue = e.target.value;\r\n setAddress(newValue);\r\n emitChange(\r\n e as ChangeEvent<HTMLInputElement>,\r\n newValue,\r\n );\r\n }\r\n }\r\n autoComplete=\"off\"\r\n spellCheck={spellCheck}\r\n onKeyDown={readonly ? undefined : handleKeyDown}\r\n onBlur={readonly ? undefined : handleBlur}\r\n sx={{\r\n ...readonlyStyles,\r\n ...(readonly && { pointerEvents: \"none\" }),\r\n }}\r\n slotProps={{\r\n ...props.slotProps,\r\n input: {\r\n ...props.slotProps?.input,\r\n readOnly: readonly,\r\n endAdornment: readonly ? undefined : (\r\n <InputAdornment\r\n position=\"end\"\r\n sx={{ mr: size === \"small\" ? -0.5 : 0.5 }}\r\n >\r\n <IconButton\r\n onClick={handleSearchButtonClick}\r\n edge=\"end\"\r\n aria-label=\"\uC8FC\uC18C \uCC3E\uAE30\"\r\n size={size}\r\n >\r\n <SearchIcon\r\n sx={{ fontSize: iconSize }}\r\n />\r\n </IconButton>\r\n </InputAdornment>\r\n ),\r\n },\r\n }}\r\n />\r\n\r\n <Dialog\r\n open={isPostcodeOpen}\r\n onClose={handlePostcodeClose}\r\n maxWidth=\"sm\"\r\n fullWidth\r\n slotProps={{\r\n paper: {\r\n sx: { height: \"550px\" },\r\n },\r\n }}\r\n >\r\n <DialogTitle\r\n sx={{\r\n display: \"flex\",\r\n justifyContent: \"space-between\",\r\n alignItems: \"center\",\r\n pr: 2,\r\n }}\r\n >\r\n \uC8FC\uC18C \uAC80\uC0C9\r\n <IconButton onClick={onClose} size=\"small\">\r\n <CloseIcon />\r\n </IconButton>\r\n </DialogTitle>\r\n <DialogContent style={{ padding: 0 }} dividers>\r\n <DaumPostcode\r\n onComplete={handlePostcodeComplete}\r\n onClose={handlePostcodeClose}\r\n defaultQuery={address as string}\r\n style={{\r\n width: \"100%\",\r\n height: \"100%\",\r\n }}\r\n />\r\n </DialogContent>\r\n </Dialog>\r\n </>\r\n );\r\n },\r\n);\r\n\r\nconst AddressTextFieldWithForm = forwardRef<\r\n HTMLDivElement,\r\n AddressTextFieldProps\r\n>(function AddressTextFieldWithForm({ form, name, onChange, ...rest }, ref) {\r\n if (!form || typeof form.useFormValue !== \"function\") {\r\n throw new Error(\r\n \"AddressTextField form prop is missing. Provide a form from useForm or useGlobalForm.\",\r\n );\r\n }\r\n if (!name) {\r\n throw new Error(\r\n \"AddressTextField requires a name when form prop is provided.\",\r\n );\r\n }\r\n\r\n const formValue = form.useFormValue(name);\r\n const handleFormChange = useCallback(\r\n (event: React.ChangeEvent<HTMLInputElement>) => {\r\n form.handleFormChange(event);\r\n onChange?.(event);\r\n },\r\n [form, onChange],\r\n );\r\n\r\n return (\r\n <AddressTextFieldBase\r\n {...rest}\r\n ref={ref}\r\n name={name}\r\n value={formValue}\r\n onChange={handleFormChange}\r\n />\r\n );\r\n});\r\n\r\nexport const AddressTextField = forwardRef<\r\n HTMLDivElement,\r\n AddressTextFieldProps\r\n>(function AddressTextField(props, ref) {\r\n if (props.form) {\r\n return <AddressTextFieldWithForm {...props} ref={ref} />;\r\n }\r\n\r\n return <AddressTextFieldBase {...props} ref={ref} />;\r\n});\r\n", "/**\r\n * useTextFieldBase.ts\r\n *\r\n * TextField \uCEF4\uD3EC\uB10C\uD2B8\uB4E4\uC758 \uACF5\uD1B5 \uB85C\uC9C1\uC744 \uB2F4\uB2F9\uD558\uB294 \uD6C5\r\n * - \uB514\uBC14\uC6B4\uC2A4 \uCC98\uB9AC\r\n * - \uB0B4\uBD80 \uC0C1\uD0DC \uAD00\uB9AC\r\n * - blur \uC2DC flush\r\n * - \uC678\uBD80 value \uB3D9\uAE30\uD654\r\n *\r\n * @license MIT\r\n * @copyright 2025 \uAE40\uC601\uC9C4 (Kim Young Jin)\r\n * @author \uAE40\uC601\uC9C4 (ehfuse@gmail.com)\r\n */\r\n\r\nimport { useState, useRef, useEffect, useCallback, ChangeEvent } from \"react\";\r\n\r\nexport interface UseTextFieldBaseOptions {\r\n value: string; // \uC678\uBD80\uC5D0\uC11C \uC804\uB2EC\uBC1B\uC740 value\r\n name?: string; // \uD544\uB4DC \uC774\uB984 (name \uC18D\uC131)\r\n debounce?: number; // \uB514\uBC14\uC6B4\uC2A4 \uC9C0\uC5F0 \uC2DC\uAC04 (ms). undefined\uBA74 \uB514\uBC14\uC6B4\uC2A4 \uC5C6\uC74C\r\n onChange?: (e: ChangeEvent<HTMLInputElement>) => void; // \uAC12 \uBCC0\uACBD \uC2DC \uD638\uCD9C\uB418\uB294 \uCF5C\uBC31\r\n onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void; // blur \uC774\uBCA4\uD2B8 \uCF5C\uBC31\r\n // \uB0B4\uBD80 \uAC12\uC744 \uBCC0\uD658\uD558\uB294 \uD568\uC218 (\uC608: \uC804\uD654\uBC88\uD638 \uD3EC\uB9F7\uD305, \uC22B\uC790 \uD3EC\uB9F7\uD305 \uB4F1)\r\n transformValue?: (\r\n inputValue: string,\r\n currentDisplayValue: string,\r\n ) => {\r\n displayValue: string;\r\n emitValue: string;\r\n };\r\n}\r\n\r\nexport interface UseTextFieldBaseReturn {\r\n internalValue: string; // \uD654\uBA74\uC5D0 \uD45C\uC2DC\uD560 \uB0B4\uBD80 \uAC12\r\n setInternalValue: (value: string) => void; // \uB0B4\uBD80 \uAC12\uC744 \uC9C1\uC811 \uC124\uC815\r\n handleChange: (e: ChangeEvent<HTMLInputElement>) => void; // TextField onChange \uD578\uB4E4\uB7EC\r\n handleBlur: (e: React.FocusEvent<HTMLInputElement>) => void; // TextField onBlur \uD578\uB4E4\uB7EC\r\n emitChange: (\r\n // \uD2B9\uC815 \uAC12\uC73C\uB85C \uBCC0\uACBD\uC744 emit (immediate=true\uBA74 \uB514\uBC14\uC6B4\uC2A4 \uBB34\uC2DC)\r\n e: ChangeEvent<HTMLInputElement> | React.FocusEvent<HTMLInputElement>,\r\n newValue: string,\r\n immediate?: boolean,\r\n ) => void;\r\n lastEmittedValue: React.MutableRefObject<string>; // \uB9C8\uC9C0\uB9C9\uC73C\uB85C emit\uB41C \uAC12\r\n debounceTimer: React.MutableRefObject<ReturnType<typeof setTimeout> | null>; // \uB514\uBC14\uC6B4\uC2A4 \uD0C0\uC774\uBA38 ref\r\n}\r\n\r\nexport function useTextFieldBase({\r\n value,\r\n name = \"\",\r\n debounce,\r\n onChange,\r\n onBlur,\r\n transformValue,\r\n}: UseTextFieldBaseOptions): UseTextFieldBaseReturn {\r\n // \uD654\uBA74\uC5D0 \uD45C\uC2DC\uB418\uB294 \uB0B4\uBD80 \uAC12\r\n const [internalValue, setInternalValue] = useState<string>(value ?? \"\");\r\n\r\n // \uB9C8\uC9C0\uB9C9\uC73C\uB85C onChange\uAC00 \uD638\uCD9C\uB41C \uAC12 (\uC911\uBCF5 \uD638\uCD9C \uBC29\uC9C0)\r\n const lastEmittedValue = useRef<string>(value ?? \"\");\r\n\r\n // \uB514\uBC14\uC6B4\uC2A4 \uD0C0\uC774\uBA38\r\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n\r\n // \uC774\uC804 \uC678\uBD80 value (\uC678\uBD80\uC5D0\uC11C \uBCC0\uACBD\uB41C \uAC83\uC778\uC9C0 \uAC10\uC9C0)\r\n const prevExternalValue = useRef<string>(value ?? \"\");\r\n\r\n // \uCEF4\uD3EC\uB10C\uD2B8 \uC5B8\uB9C8\uC6B4\uD2B8 \uC2DC \uD0C0\uC774\uBA38 \uC815\uB9AC\r\n useEffect(() => {\r\n return () => {\r\n if (debounceTimer.current) {\r\n clearTimeout(debounceTimer.current);\r\n }\r\n };\r\n }, []);\r\n\r\n // \uC678\uBD80 value prop \uBCC0\uACBD \uC2DC \uB0B4\uBD80 \uC0C1\uD0DC \uB3D9\uAE30\uD654\r\n useEffect(() => {\r\n const newValue = value ?? \"\";\r\n // \uC678\uBD80\uC5D0\uC11C \uAC12\uC774 \uBCC0\uACBD\uB418\uC5C8\uACE0, \uADF8 \uAC12\uC774 \uC6B0\uB9AC\uAC00 \uB9C8\uC9C0\uB9C9\uC73C\uB85C emit\uD55C \uAC12\uACFC \uB2E4\uB974\uBA74 \uB3D9\uAE30\uD654\r\n if (\r\n newValue !== prevExternalValue.current &&\r\n newValue !== lastEmittedValue.current\r\n ) {\r\n setInternalValue(newValue);\r\n lastEmittedValue.current = newValue;\r\n }\r\n prevExternalValue.current = newValue;\r\n }, [value]);\r\n\r\n // onChange\uB97C emit\uD558\uB294 \uD568\uC218\r\n const emitChange = useCallback(\r\n (\r\n e:\r\n | ChangeEvent<HTMLInputElement>\r\n | React.FocusEvent<HTMLInputElement>,\r\n newValue: string,\r\n immediate = false,\r\n ) => {\r\n // \uAC19\uC740 \uAC12\uC774\uBA74 \uD638\uCD9C\uD558\uC9C0 \uC54A\uC74C\r\n if (newValue === lastEmittedValue.current) {\r\n return;\r\n }\r\n\r\n const doEmit = () => {\r\n if (newValue !== lastEmittedValue.current) {\r\n lastEmittedValue.current = newValue;\r\n if (onChange) {\r\n const syntheticEvent = {\r\n ...e,\r\n target: {\r\n ...e.target,\r\n name: name,\r\n value: newValue,\r\n },\r\n } as ChangeEvent<HTMLInputElement>;\r\n onChange(syntheticEvent);\r\n }\r\n }\r\n };\r\n\r\n if (debounceTimer.current) {\r\n clearTimeout(debounceTimer.current);\r\n }\r\n\r\n if (immediate || debounce === undefined || debounce === 0) {\r\n doEmit();\r\n } else {\r\n debounceTimer.current = setTimeout(doEmit, debounce);\r\n }\r\n },\r\n [onChange, debounce, name],\r\n );\r\n\r\n // TextField onChange \uD578\uB4E4\uB7EC\r\n const handleChange = useCallback(\r\n (e: ChangeEvent<HTMLInputElement>) => {\r\n const inputValue = e.target.value;\r\n\r\n if (transformValue) {\r\n // \uBCC0\uD658 \uD568\uC218\uAC00 \uC788\uC73C\uBA74 \uC0AC\uC6A9\r\n const { displayValue, emitValue } = transformValue(\r\n inputValue,\r\n internalValue,\r\n );\r\n setInternalValue(displayValue);\r\n emitChange(e, emitValue);\r\n } else {\r\n // \uBCC0\uD658 \uC5C6\uC774 \uADF8\uB300\uB85C \uC0AC\uC6A9\r\n setInternalValue(inputValue);\r\n emitChange(e, inputValue);\r\n }\r\n },\r\n [transformValue, internalValue, emitChange],\r\n );\r\n\r\n // blur \uC2DC \uB300\uAE30 \uC911\uC778 \uB514\uBC14\uC6B4\uC2A4 \uC989\uC2DC \uC2E4\uD589\r\n const handleBlur = useCallback(\r\n (e: React.FocusEvent<HTMLInputElement>) => {\r\n if (debounceTimer.current) {\r\n clearTimeout(debounceTimer.current);\r\n debounceTimer.current = null;\r\n\r\n // \uD604\uC7AC \uB0B4\uBD80 \uAC12\uC73C\uB85C \uC989\uC2DC emit\r\n if (internalValue !== lastEmittedValue.current) {\r\n lastEmittedValue.current = internalValue;\r\n if (onChange) {\r\n const syntheticEvent = {\r\n ...e,\r\n target: {\r\n ...e.target,\r\n name: name,\r\n value: internalValue,\r\n },\r\n } as ChangeEvent<HTMLInputElement>;\r\n onChange(syntheticEvent);\r\n }\r\n }\r\n }\r\n\r\n // \uC6D0\uB798 onBlur \uD638\uCD9C\r\n if (onBlur) {\r\n onBlur(e);\r\n }\r\n },\r\n [internalValue, onChange, onBlur, name],\r\n );\r\n\r\n return {\r\n internalValue,\r\n setInternalValue,\r\n handleChange,\r\n handleBlur,\r\n emitChange,\r\n lastEmittedValue,\r\n debounceTimer,\r\n };\r\n}\r\n\r\n// \uB514\uBC14\uC6B4\uC2A4\uB41C onChange emit\uB9CC \uB2F4\uB2F9\uD558\uB294 \uAC04\uB2E8\uD55C \uD6C5 (\uBCF5\uC7A1\uD55C \uD3EC\uB9F7\uD305 \uB85C\uC9C1\uC774 \uC788\uB294 \uCEF4\uD3EC\uB10C\uD2B8\uC5D0\uC11C \uC0AC\uC6A9)\r\nexport interface UseDebouncedEmitOptions {\r\n name?: string; // \uD544\uB4DC \uC774\uB984 (name \uC18D\uC131)\r\n debounce?: number; // \uB514\uBC14\uC6B4\uC2A4 \uC9C0\uC5F0 \uC2DC\uAC04 (ms). undefined\uBA74 \uB514\uBC14\uC6B4\uC2A4 \uC5C6\uC74C\r\n onChange?: (e: ChangeEvent<HTMLInputElement>) => void; // \uAC12 \uBCC0\uACBD \uC2DC \uD638\uCD9C\uB418\uB294 \uCF5C\uBC31\r\n}\r\n\r\nexport interface UseDebouncedEmitReturn {\r\n emitChange: (\r\n // \uB514\uBC14\uC6B4\uC2A4\uB41C onChange emit\r\n e:\r\n | ChangeEvent<HTMLInputElement>\r\n | React.FocusEvent<HTMLInputElement>\r\n | null,\r\n newValue: string,\r\n immediate?: boolean,\r\n ) => void;\r\n flushOnBlur: (\r\n // blur \uC2DC \uB300\uAE30 \uC911\uC778 \uB514\uBC14\uC6B4\uC2A4 flush\r\n e: React.FocusEvent<HTMLInputElement>,\r\n currentValue: string,\r\n ) => void;\r\n lastEmittedValue: React.MutableRefObject<string>; // \uB9C8\uC9C0\uB9C9\uC73C\uB85C emit\uB41C \uAC12\r\n debounceTimer: React.MutableRefObject<ReturnType<typeof setTimeout> | null>; // \uB514\uBC14\uC6B4\uC2A4 \uD0C0\uC774\uBA38 ref\r\n}\r\n\r\nexport function useDebouncedEmit({\r\n name = \"\",\r\n debounce,\r\n onChange,\r\n}: UseDebouncedEmitOptions): UseDebouncedEmitReturn {\r\n // \uB9C8\uC9C0\uB9C9\uC73C\uB85C onChange\uAC00 \uD638\uCD9C\uB41C \uAC12 (\uC911\uBCF5 \uD638\uCD9C \uBC29\uC9C0)\r\n const lastEmittedValue = useRef<string>(\"\");\r\n\r\n // \uB514\uBC14\uC6B4\uC2A4 \uD0C0\uC774\uBA38\r\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n\r\n // \uCEF4\uD3EC\uB10C\uD2B8 \uC5B8\uB9C8\uC6B4\uD2B8 \uC2DC \uD0C0\uC774\uBA38 \uC815\uB9AC\r\n useEffect(() => {\r\n return () => {\r\n if (debounceTimer.current) {\r\n clearTimeout(debounceTimer.current);\r\n }\r\n };\r\n }, []);\r\n\r\n // onChange\uB97C emit\uD558\uB294 \uD568\uC218\r\n const emitChange = useCallback(\r\n (\r\n e:\r\n | ChangeEvent<HTMLInputElement>\r\n | React.FocusEvent<HTMLInputElement>\r\n | null,\r\n newValue: string,\r\n immediate = false,\r\n ) => {\r\n // \uAC19\uC740 \uAC12\uC774\uBA74 \uD638\uCD9C\uD558\uC9C0 \uC54A\uC74C\r\n if (newValue === lastEmittedValue.current) {\r\n return;\r\n }\r\n\r\n const doEmit = () => {\r\n if (newValue !== lastEmittedValue.current) {\r\n lastEmittedValue.current = newValue;\r\n if (onChange) {\r\n const syntheticEvent = {\r\n ...(e || {}),\r\n target: {\r\n ...(e?.target || {}),\r\n name: name,\r\n value: newValue,\r\n },\r\n } as ChangeEvent<HTMLInputElement>;\r\n onChange(syntheticEvent);\r\n }\r\n }\r\n };\r\n\r\n if (debounceTimer.current) {\r\n clearTimeout(debounceTimer.current);\r\n }\r\n\r\n if (immediate || debounce === undefined || debounce === 0) {\r\n doEmit();\r\n } else {\r\n debounceTimer.current = setTimeout(doEmit, debounce);\r\n }\r\n },\r\n [onChange, debounce, name],\r\n );\r\n\r\n // blur \uC2DC \uB300\uAE30 \uC911\uC778 \uB514\uBC14\uC6B4\uC2A4 flush\r\n const flushOnBlur = useCallback(\r\n (e: React.FocusEvent<HTMLInputElement>, currentValue: string) => {\r\n if (debounceTimer.current) {\r\n clearTimeout(debounceTimer.current);\r\n debounceTimer.current = null;\r\n\r\n if (currentValue !== lastEmittedValue.current) {\r\n lastEmittedValue.current = currentValue;\r\n if (onChange) {\r\n const syntheticEvent = {\r\n ...e,\r\n target: {\r\n ...e.target,\r\n name: name,\r\n value: currentValue,\r\n },\r\n } as ChangeEvent<HTMLInputElement>;\r\n onChange(syntheticEvent);\r\n }\r\n }\r\n }\r\n },\r\n [onChange, name],\r\n );\r\n\r\n return {\r\n emitChange,\r\n flushOnBlur,\r\n lastEmittedValue,\r\n debounceTimer,\r\n };\r\n}\r\n", "/**\r\n * useKoreanHolidays.ts\r\n *\r\n * \uD55C\uAD6D\uCC9C\uBB38\uC5F0\uAD6C\uC6D0 \uD2B9\uC77C \uC815\uBCF4 API\uB97C \uC774\uC6A9\uD558\uC5EC \uACF5\uD734\uC77C\uC744 \uC870\uD68C\uD558\uB294 \uCEE4\uC2A4\uD140 \uD6C5\r\n *\r\n * @license MIT\r\n * @copyright 2025 \uAE40\uC601\uC9C4 (Kim Young Jin)\r\n * @author \uAE40\uC601\uC9C4 (ehfuse@gmail.com)\r\n */\r\n\r\nimport { useState, useEffect, useCallback } from \"react\";\r\n\r\n// \uB85C\uCEEC\uC2A4\uD1A0\uB9AC\uC9C0 \uD0A4 prefix\r\nconst STORAGE_KEY_PREFIX = \"korean-holidays-\";\r\n\r\n// API \uC751\uB2F5 \uD0C0\uC785\r\ninterface HolidayItem {\r\n dateKind: string;\r\n dateName: string;\r\n isHoliday: string;\r\n locdate: number;\r\n seq: number;\r\n}\r\n\r\ninterface ApiResponse {\r\n response: {\r\n header: {\r\n resultCode: string;\r\n resultMsg: string;\r\n };\r\n body: {\r\n items: {\r\n item: HolidayItem | HolidayItem[];\r\n };\r\n numOfRows: number;\r\n pageNo: number;\r\n totalCount: number;\r\n };\r\n };\r\n}\r\n\r\n// \uCE90\uC2DC\uB41C \uACF5\uD734\uC77C \uB370\uC774\uD130 \uD0C0\uC785\r\ninterface CachedHolidays {\r\n year: number;\r\n holidays: string[]; // YYYYMMDD \uD615\uC2DD\r\n fetchedAt: number; // timestamp\r\n}\r\n\r\n/**\r\n * \uD55C\uAD6D \uACF5\uD734\uC77C\uC744 \uC870\uD68C\uD558\uB294 \uCEE4\uC2A4\uD140 \uD6C5\r\n *\r\n * @param apiKey - \uACF5\uACF5\uB370\uC774\uD130\uD3EC\uD138 API \uC778\uC99D\uD0A4 (Encoding)\r\n * @param year - \uC870\uD68C\uD560 \uC5F0\uB3C4 (\uAE30\uBCF8\uAC12: \uD604\uC7AC \uC5F0\uB3C4)\r\n * @returns { holidays: Date[], loading: boolean, error: string | null }\r\n */\r\nexport function useKoreanHolidays(apiKey?: string, year?: number) {\r\n const targetYear = year ?? new Date().getFullYear();\r\n const [holidays, setHolidays] = useState<Date[]>([]);\r\n const [loading, setLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n // \uB85C\uCEEC\uC2A4\uD1A0\uB9AC\uC9C0\uC5D0\uC11C \uCE90\uC2DC\uB41C \uB370\uC774\uD130 \uAC00\uC838\uC624\uAE30\r\n const getCachedHolidays = useCallback((year: number): Date[] | null => {\r\n try {\r\n const cached = localStorage.getItem(`${STORAGE_KEY_PREFIX}${year}`);\r\n if (!cached) return null;\r\n\r\n const data: CachedHolidays = JSON.parse(cached);\r\n\r\n // 1\uB144 \uC774\uC0C1 \uB41C \uCE90\uC2DC\uB294 \uBB34\uD6A8\uD654 (\uB2E4\uC74C \uD574\uC5D0 \uB2E4\uC2DC \uC870\uD68C)\r\n const oneYearMs = 365 * 24 * 60 * 60 * 1000;\r\n if (Date.now() - data.fetchedAt > oneYearMs) {\r\n localStorage.removeItem(`${STORAGE_KEY_PREFIX}${year}`);\r\n return null;\r\n }\r\n\r\n // YYYYMMDD \uBB38\uC790\uC5F4\uC744 Date \uAC1D\uCCB4\uB85C \uBCC0\uD658\r\n return data.holidays.map((dateStr) => {\r\n const y = parseInt(dateStr.slice(0, 4), 10);\r\n const m = parseInt(dateStr.slice(4, 6), 10) - 1;\r\n const d = parseInt(dateStr.slice(6, 8), 10);\r\n return new Date(y, m, d);\r\n });\r\n } catch {\r\n return null;\r\n }\r\n }, []);\r\n\r\n // \uB85C\uCEEC\uC2A4\uD1A0\uB9AC\uC9C0\uC5D0 \uCE90\uC2DC \uC800\uC7A5\r\n const setCachedHolidays = useCallback(\r\n (year: number, holidays: string[]) => {\r\n try {\r\n const data: CachedHolidays = {\r\n year,\r\n holidays,\r\n fetchedAt: Date.now(),\r\n };\r\n localStorage.setItem(\r\n `${STORAGE_KEY_PREFIX}${year}`,\r\n JSON.stringify(data)\r\n );\r\n } catch {\r\n // \uB85C\uCEEC\uC2A4\uD1A0\uB9AC\uC9C0 \uC6A9\uB7C9 \uCD08\uACFC \uB4F1\uC758 \uC5D0\uB7EC \uBB34\uC2DC\r\n }\r\n },\r\n []\r\n );\r\n\r\n // API\uC5D0\uC11C \uACF5\uD734\uC77C \uC870\uD68C\r\n const fetchHolidays = useCallback(\r\n async (year: number): Promise<Date[]> => {\r\n if (!apiKey) {\r\n throw new Error(\"API \uD0A4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.\");\r\n }\r\n\r\n const baseUrl =\r\n \"https://apis.data.go.kr/B090041/openapi/service/SpcdeInfoService/getRestDeInfo\";\r\n const params = new URLSearchParams({\r\n serviceKey: apiKey,\r\n solYear: String(year),\r\n numOfRows: \"100\",\r\n _type: \"json\",\r\n });\r\n\r\n const response = await fetch(`${baseUrl}?${params.toString()}`);\r\n\r\n if (!response.ok) {\r\n throw new Error(`API \uC694\uCCAD \uC2E4\uD328: ${response.status}`);\r\n }\r\n\r\n const data: ApiResponse = await response.json();\r\n\r\n if (data.response.header.resultCode !== \"00\") {\r\n throw new Error(`API \uC5D0\uB7EC: ${data.response.header.resultMsg}`);\r\n }\r\n\r\n const items = data.response.body.items?.item;\r\n if (!items) {\r\n return [];\r\n }\r\n\r\n // \uB2E8\uC77C \uD56D\uBAA9\uC77C \uACBD\uC6B0 \uBC30\uC5F4\uB85C \uBCC0\uD658\r\n const itemArray = Array.isArray(items) ? items : [items];\r\n\r\n // isHoliday\uAC00 \"Y\"\uC778 \uD56D\uBAA9\uB9CC \uD544\uD130\uB9C1\r\n const holidayDates = itemArray\r\n .filter((item) => item.isHoliday === \"Y\")\r\n .map((item) => {\r\n const dateStr = String(item.locdate);\r\n const y = parseInt(dateStr.slice(0, 4), 10);\r\n const m = parseInt(dateStr.slice(4, 6), 10) - 1;\r\n const d = parseInt(dateStr.slice(6, 8), 10);\r\n return new Date(y, m, d);\r\n });\r\n\r\n // \uCE90\uC2DC\uC5D0 \uC800\uC7A5 (YYYYMMDD \uD615\uC2DD\uC73C\uB85C)\r\n const holidayStrings = itemArray\r\n .filter((item) => item.isHoliday === \"Y\")\r\n .map((item) => String(item.locdate));\r\n setCachedHolidays(year, holidayStrings);\r\n\r\n return holidayDates;\r\n },\r\n [apiKey, setCachedHolidays]\r\n );\r\n\r\n useEffect(() => {\r\n if (!apiKey) {\r\n setHolidays([]);\r\n return;\r\n }\r\n\r\n // \uCE90\uC2DC\uB41C \uB370\uC774\uD130 \uD655\uC778\r\n const cached = getCachedHolidays(targetYear);\r\n if (cached) {\r\n setHolidays(cached);\r\n return;\r\n }\r\n\r\n // API \uD638\uCD9C\r\n setLoading(true);\r\n setError(null);\r\n\r\n fetchHolidays(targetYear)\r\n .then((dates) => {\r\n setHolidays(dates);\r\n })\r\n .catch((err) => {\r\n setError(err.message);\r\n setHolidays([]);\r\n })\r\n .finally(() => {\r\n setLoading(false);\r\n });\r\n }, [apiKey, targetYear, getCachedHolidays, fetchHolidays]);\r\n\r\n return { holidays, loading, error };\r\n}\r\n\r\n/**\r\n * \uC5EC\uB7EC \uC5F0\uB3C4\uC758 \uACF5\uD734\uC77C\uC744 \uD55C\uBC88\uC5D0 \uC870\uD68C\uD558\uB294 \uD6C5\r\n */\r\nexport function useKoreanHolidaysRange(\r\n apiKey?: string,\r\n startYear?: number,\r\n endYear?: number\r\n) {\r\n const currentYear = new Date().getFullYear();\r\n const start = startYear ?? currentYear;\r\n const end = endYear ?? currentYear;\r\n\r\n const [holidays, setHolidays] = useState<Date[]>([]);\r\n const [loading, setLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n useEffect(() => {\r\n if (!apiKey) {\r\n setHolidays([]);\r\n return;\r\n }\r\n\r\n const years: number[] = [];\r\n for (let y = start; y <= end; y++) {\r\n years.push(y);\r\n }\r\n\r\n setLoading(true);\r\n setError(null);\r\n\r\n // \uAC01 \uC5F0\uB3C4\uBCC4\uB85C \uCE90\uC2DC \uD655\uC778 \uB610\uB294 API \uD638\uCD9C\r\n Promise.all(\r\n years.map(async (year) => {\r\n const storageKey = `${STORAGE_KEY_PREFIX}${year}`;\r\n const cached = localStorage.getItem(storageKey);\r\n\r\n if (cached) {\r\n try {\r\n const data: CachedHolidays = JSON.parse(cached);\r\n return data.holidays.map((dateStr) => {\r\n const y = parseInt(dateStr.slice(0, 4), 10);\r\n const m = parseInt(dateStr.slice(4, 6), 10) - 1;\r\n const d = parseInt(dateStr.slice(6, 8), 10);\r\n return new Date(y, m, d);\r\n });\r\n } catch {\r\n // \uD30C\uC2F1 \uC2E4\uD328 \uC2DC API \uD638\uCD9C\r\n }\r\n }\r\n\r\n // API \uD638\uCD9C\r\n const baseUrl =\r\n \"https://apis.data.go.kr/B090041/openapi/service/SpcdeInfoService/getRestDeInfo\";\r\n const params = new URLSearchParams({\r\n serviceKey: apiKey,\r\n solYear: String(year),\r\n numOfRows: \"100\",\r\n _type: \"json\",\r\n });\r\n\r\n const response = await fetch(`${baseUrl}?${params.toString()}`);\r\n if (!response.ok) {\r\n throw new Error(`API \uC694\uCCAD \uC2E4\uD328: ${response.status}`);\r\n }\r\n\r\n const data: ApiResponse = await response.json();\r\n if (data.response.header.resultCode !== \"00\") {\r\n throw new Error(data.response.header.resultMsg);\r\n }\r\n\r\n const items = data.response.body.items?.item;\r\n if (!items) return [];\r\n\r\n const itemArray = Array.isArray(items) ? items : [items];\r\n const holidayStrings = itemArray\r\n .filter((item) => item.isHoliday === \"Y\")\r\n .map((item) => String(item.locdate));\r\n\r\n // \uCE90\uC2DC \uC800\uC7A5\r\n const cacheData: CachedHolidays = {\r\n year,\r\n holidays: holidayStrings,\r\n fetchedAt: Date.now(),\r\n };\r\n localStorage.setItem(storageKey, JSON.stringify(cacheData));\r\n\r\n return holidayStrings.map((dateStr) => {\r\n const y = parseInt(dateStr.slice(0, 4), 10);\r\n const m = parseInt(dateStr.slice(4, 6), 10) - 1;\r\n const d = parseInt(dateStr.slice(6, 8), 10);\r\n return new Date(y, m, d);\r\n });\r\n })\r\n )\r\n .then((results) => {\r\n const allHolidays = results.flat();\r\n setHolidays(allHolidays);\r\n })\r\n .catch((err) => {\r\n setError(err.message);\r\n setHolidays([]);\r\n })\r\n .finally(() => {\r\n setLoading(false);\r\n });\r\n }, [apiKey, start, end]);\r\n\r\n return { holidays, loading, error };\r\n}\r\n", "/**\r\n * useGroupedInput.ts\r\n *\r\n * \uADF8\uB8F9\uD654\uB41C \uC785\uB825 \uD544\uB4DC(\uC8FC\uBBFC\uBC88\uD638, \uC0AC\uC5C5\uC790\uBC88\uD638, \uC2DC\uAC04 \uB4F1)\uC5D0\uC11C \uACF5\uD1B5\uC73C\uB85C \uC0AC\uC6A9\uD558\uB294\r\n * \uCEE4\uC11C \uAD00\uB9AC, \uD3EC\uCEE4\uC2A4 \uAD00\uB9AC, \uD0A4\uBCF4\uB4DC \uD578\uB4E4\uB9C1 \uB85C\uC9C1\uC744 \uC81C\uACF5\uD558\uB294 \uD6C5\r\n *\r\n * @license MIT\r\n * @copyright 2025 \uAE40\uC601\uC9C4 (Kim Young Jin)\r\n * @author \uAE40\uC601\uC9C4 (ehfuse@gmail.com)\r\n */\r\n\r\nimport { useRef, useState, useCallback, useMemo, RefObject } from \"react\";\r\n\r\nexport interface GroupConfig {\r\n maxLength: number;\r\n ref: RefObject<HTMLInputElement | null>;\r\n value: string;\r\n setValue: (value: string) => void;\r\n}\r\n\r\nexport interface UseGroupedInputOptions {\r\n groups: GroupConfig[];\r\n fontFamily?: string;\r\n fontSize: number;\r\n onComplete?: () => void;\r\n disabled?: boolean;\r\n readonly?: boolean;\r\n}\r\n\r\nexport interface UseGroupedInputReturn {\r\n focusedGroup: number | null;\r\n setFocusedGroup: (group: number | null) => void;\r\n cursorVisible: boolean;\r\n setCursorVisible: (visible: boolean) => void;\r\n cursorPosRef: RefObject<number>;\r\n isClickFocusRef: RefObject<boolean>;\r\n inputComplete: boolean;\r\n setInputComplete: (complete: boolean) => void;\r\n renderTrigger: number;\r\n measureTextWidth: (text: string) => number;\r\n getCursorLeft: (groupIndex: number) => number;\r\n createMouseDownHandler: (\r\n groupIndex: number\r\n ) => (e: React.MouseEvent) => void;\r\n createClickHandler: (groupIndex: number) => (e: React.MouseEvent) => void;\r\n createContainerClickHandler: () => (e: React.MouseEvent) => void;\r\n createFocusHandler: (groupIndex: number) => () => void;\r\n createBlurHandler: () => () => void;\r\n createKeyDownHandler: (\r\n groupIndex: number,\r\n onValueChange?: (newValue: string, groupIndex: number) => void\r\n ) => (e: React.KeyboardEvent<HTMLInputElement>) => void;\r\n createChangeHandler: (\r\n groupIndex: number,\r\n onAfterChange?: (\r\n newValue: string,\r\n groupIndex: number,\r\n isComplete: boolean\r\n ) => void\r\n ) => (e: React.ChangeEvent<HTMLInputElement>) => void;\r\n getCursorPosFromClick: (\r\n e: React.MouseEvent,\r\n value: string,\r\n inputRef: RefObject<HTMLInputElement | null>\r\n ) => number;\r\n forceRender: () => void;\r\n}\r\n\r\nexport function useGroupedInput({\r\n groups,\r\n fontFamily,\r\n fontSize,\r\n onComplete,\r\n disabled = false,\r\n readonly = false,\r\n}: UseGroupedInputOptions): UseGroupedInputReturn {\r\n const [focusedGroup, setFocusedGroup] = useState<number | null>(null);\r\n const [cursorVisible, setCursorVisible] = useState(false);\r\n const [inputComplete, setInputComplete] = useState(false);\r\n const [renderTrigger, setRenderTrigger] = useState(0);\r\n\r\n const cursorPosRef = useRef(0);\r\n const isClickFocusRef = useRef(false);\r\n const isArrowFocusRef = useRef(false);\r\n const arrowTargetPosRef = useRef(0);\r\n\r\n // \uD14D\uC2A4\uD2B8 \uB108\uBE44 \uCE21\uC815\r\n const measureTextWidth = useCallback(\r\n (text: string): number => {\r\n if (typeof document === \"undefined\")\r\n return text.length * fontSize * 0.6;\r\n const canvas = document.createElement(\"canvas\");\r\n const ctx = canvas.getContext(\"2d\");\r\n if (!ctx) return text.length * fontSize * 0.6;\r\n ctx.font = `${fontSize}px ${fontFamily || \"Roboto, sans-serif\"}`;\r\n return ctx.measureText(text).width;\r\n },\r\n [fontSize, fontFamily]\r\n );\r\n\r\n // \uD074\uB9AD \uC704\uCE58\uC5D0\uC11C \uCEE4\uC11C \uC704\uCE58 \uACC4\uC0B0\r\n const getCursorPosFromClick = useCallback(\r\n (\r\n e: React.MouseEvent,\r\n value: string,\r\n inputRef: RefObject<HTMLInputElement | null>\r\n ): number => {\r\n const input = inputRef.current;\r\n if (!input) return value.length;\r\n\r\n const rect = input.getBoundingClientRect();\r\n const clickX = e.clientX - rect.left - 4; // padding \uBCF4\uC815\r\n\r\n let newPos = value.length;\r\n for (let i = 0; i <= value.length; i++) {\r\n const textWidth = measureTextWidth(value.slice(0, i));\r\n if (clickX < textWidth + measureTextWidth(\"0\") / 2) {\r\n newPos = i;\r\n break;\r\n }\r\n }\r\n return newPos;\r\n },\r\n [measureTextWidth]\r\n );\r\n\r\n // \uCEE4\uC11C \uC704\uCE58 \uACC4\uC0B0 (\uB80C\uB354\uB9C1\uC6A9)\r\n const getCursorLeft = useCallback(\r\n (groupIndex: number): number => {\r\n if (focusedGroup !== groupIndex) return 0;\r\n const group = groups[groupIndex];\r\n if (!group) return 0;\r\n const textBeforeCursor = group.value.slice(0, cursorPosRef.current);\r\n return measureTextWidth(textBeforeCursor);\r\n },\r\n [focusedGroup, groups, measureTextWidth]\r\n );\r\n\r\n // \uAC15\uC81C \uB9AC\uB80C\uB354\uB9C1\r\n const forceRender = useCallback(() => {\r\n setRenderTrigger((prev) => prev + 1);\r\n }, []);\r\n\r\n // MouseDown \uD578\uB4E4\uB7EC \uC0DD\uC131\r\n const createMouseDownHandler = useCallback(\r\n (groupIndex: number) => {\r\n return (e: React.MouseEvent) => {\r\n if (disabled || readonly) return;\r\n isClickFocusRef.current = true;\r\n const group = groups[groupIndex];\r\n const newPos = getCursorPosFromClick(e, group.value, group.ref);\r\n cursorPosRef.current = newPos;\r\n };\r\n },\r\n [disabled, readonly, groups, getCursorPosFromClick]\r\n );\r\n\r\n // Click \uD578\uB4E4\uB7EC \uC0DD\uC131 - \uD074\uB9AD \uC2DC \uC804\uCCB4 \uC120\uD0DD\r\n const createClickHandler = useCallback(\r\n (groupIndex: number) => {\r\n return (e: React.MouseEvent) => {\r\n if (disabled || readonly) return;\r\n e.stopPropagation();\r\n\r\n const group = groups[groupIndex];\r\n const input = group.ref.current;\r\n\r\n // \uAC12\uC774 \uC788\uC73C\uBA74 \uC804\uCCB4 \uC120\uD0DD\r\n if (input && group.value.length > 0) {\r\n setTimeout(() => {\r\n input.setSelectionRange(0, group.value.length);\r\n }, 0);\r\n setCursorVisible(false); // \uC804\uCCB4 \uC120\uD0DD \uC2DC \uCEE4\uC11C \uC228\uAE40\r\n } else {\r\n // \uAC12\uC774 \uC5C6\uC73C\uBA74 \uCEE4\uC11C \uD45C\uC2DC\r\n cursorPosRef.current = 0;\r\n setCursorVisible(true);\r\n }\r\n\r\n setInputComplete(false);\r\n setFocusedGroup(groupIndex);\r\n setRenderTrigger((prev) => prev + 1);\r\n };\r\n },\r\n [disabled, readonly, groups]\r\n );\r\n\r\n // \uCEE8\uD14C\uC774\uB108 \uD074\uB9AD \uD578\uB4E4\uB7EC \uC0DD\uC131 - \uC678\uACFD \uD074\uB9AD \uC2DC \uCCAB \uBC88\uC9F8 \uBE48 \uCE78 \uB610\uB294 \uB9C8\uC9C0\uB9C9 \uCE78\uC73C\uB85C \uC774\uB3D9\r\n const createContainerClickHandler = useCallback(() => {\r\n return (e: React.MouseEvent) => {\r\n if (disabled || readonly) return;\r\n\r\n // \uC774\uBBF8 input\uC5D0\uC11C \uCC98\uB9AC\uB41C \uD074\uB9AD\uC774\uBA74 \uBB34\uC2DC\r\n const target = e.target as HTMLElement;\r\n if (target.tagName === \"INPUT\") return;\r\n\r\n // \uCCAB \uBC88\uC9F8 \uBE48 \uCE78 \uCC3E\uAE30\r\n let targetIndex = -1;\r\n for (let i = 0; i < groups.length; i++) {\r\n if (groups[i].value.length === 0) {\r\n targetIndex = i;\r\n break;\r\n }\r\n }\r\n\r\n // \uBE48 \uCE78\uC774 \uC5C6\uC73C\uBA74 \uB9C8\uC9C0\uB9C9 \uCE78 \uC120\uD0DD\r\n if (targetIndex === -1) {\r\n targetIndex = groups.length - 1;\r\n }\r\n\r\n const targetGroup = groups[targetIndex];\r\n const input = targetGroup.ref.current;\r\n\r\n if (targetGroup.value.length > 0) {\r\n // \uAC12\uC774 \uC788\uC73C\uBA74 \uC804\uCCB4 \uC120\uD0DD\r\n input?.focus();\r\n setTimeout(() => {\r\n input?.setSelectionRange(0, targetGroup.value.length);\r\n }, 0);\r\n setCursorVisible(false);\r\n } else {\r\n // \uAC12\uC774 \uC5C6\uC73C\uBA74 \uCEE4\uC11C \uD45C\uC2DC\r\n cursorPosRef.current = 0;\r\n setCursorVisible(true);\r\n input?.focus();\r\n }\r\n\r\n setFocusedGroup(targetIndex);\r\n setInputComplete(false);\r\n setRenderTrigger((prev) => prev + 1);\r\n };\r\n }, [disabled, readonly, groups]);\r\n\r\n // Focus \uD578\uB4E4\uB7EC \uC0DD\uC131\r\n const createFocusHandler = useCallback(\r\n (groupIndex: number) => {\r\n return () => {\r\n const group = groups[groupIndex];\r\n\r\n // \uD074\uB9AD\uC73C\uB85C \uC778\uD55C \uD3EC\uCEE4\uC2A4\uBA74 \uD074\uB9AD \uD578\uB4E4\uB7EC\uC5D0\uC11C \uCC98\uB9AC \uC644\uB8CC\uB428\r\n if (isClickFocusRef.current) {\r\n isClickFocusRef.current = false;\r\n // \uD074\uB9AD \uD578\uB4E4\uB7EC\uC5D0\uC11C \uC804\uCCB4 \uC120\uD0DD \uCC98\uB9AC\uD588\uC73C\uBBC0\uB85C \uC5EC\uAE30\uC11C\uB294 \uC0C1\uD0DC\uB9CC \uC124\uC815\r\n setFocusedGroup(groupIndex);\r\n return;\r\n }\r\n\r\n // \uBC29\uD5A5\uD0A4\uB85C \uC778\uD55C \uD3EC\uCEE4\uC2A4\uBA74 \uC9C0\uC815\uB41C \uC704\uCE58\uB85C\r\n if (isArrowFocusRef.current) {\r\n isArrowFocusRef.current = false;\r\n cursorPosRef.current = arrowTargetPosRef.current;\r\n setCursorVisible(true);\r\n setFocusedGroup(groupIndex);\r\n setRenderTrigger((prev) => prev + 1);\r\n return;\r\n }\r\n\r\n // \uC77C\uBC18 \uD3EC\uCEE4\uC2A4 (\uD0ED \uB4F1) - \uC804\uCCB4 \uC120\uD0DD\r\n const input = group.ref.current;\r\n if (input && group.value.length > 0) {\r\n setTimeout(() => {\r\n input.setSelectionRange(0, group.value.length);\r\n }, 0);\r\n setCursorVisible(false);\r\n } else {\r\n cursorPosRef.current = 0;\r\n setCursorVisible(true);\r\n }\r\n setFocusedGroup(groupIndex);\r\n };\r\n },\r\n [groups]\r\n );\r\n\r\n // Blur \uD578\uB4E4\uB7EC \uC0DD\uC131\r\n const createBlurHandler = useCallback(() => {\r\n return () => {\r\n setTimeout(() => {\r\n const activeElement = document.activeElement;\r\n const isStillFocused = groups.some(\r\n (group) => group.ref.current === activeElement\r\n );\r\n if (!isStillFocused) {\r\n setCursorVisible(false);\r\n setFocusedGroup(null);\r\n }\r\n }, 0);\r\n };\r\n }, [groups]);\r\n\r\n // KeyDown \uD578\uB4E4\uB7EC \uC0DD\uC131\r\n const createKeyDownHandler = useCallback(\r\n (\r\n groupIndex: number,\r\n onValueChange?: (newValue: string, groupIndex: number) => void\r\n ) => {\r\n return (e: React.KeyboardEvent<HTMLInputElement>) => {\r\n const group = groups[groupIndex];\r\n const input = group.ref.current;\r\n const selectionStart = input?.selectionStart ?? 0;\r\n const selectionEnd = input?.selectionEnd ?? 0;\r\n const hasSelection = selectionEnd > selectionStart;\r\n\r\n if (e.key === \"ArrowLeft\") {\r\n // 3-2. \uD604\uC7AC\uCE78 \uCCAB\uBC88\uC9F8\uC5D0\uC11C \uC67C\uCABD\uD0A4 \u2192 \uC774\uC804\uCE78 \uB9C8\uC9C0\uB9C9 \uBB38\uC790\uB85C\r\n if (selectionStart === 0 && groupIndex > 0) {\r\n e.preventDefault();\r\n const prevGroup = groups[groupIndex - 1];\r\n // \uB9C8\uC9C0\uB9C9 \uBB38\uC790 \uC704\uCE58 (length - 1), \uBE48 \uACBD\uC6B0 0\r\n const newPos =\r\n prevGroup.value.length > 0\r\n ? prevGroup.value.length - 1\r\n : 0;\r\n isArrowFocusRef.current = true;\r\n arrowTargetPosRef.current = newPos;\r\n prevGroup.ref.current?.focus();\r\n prevGroup.ref.current?.setSelectionRange(\r\n newPos,\r\n newPos\r\n );\r\n } else if (selectionStart > 0) {\r\n e.preventDefault();\r\n const newPos = selectionStart - 1;\r\n input?.setSelectionRange(newPos, newPos);\r\n cursorPosRef.current = newPos;\r\n setCursorVisible(true);\r\n setRenderTrigger((prev) => prev + 1);\r\n }\r\n } else if (e.key === \"ArrowRight\") {\r\n // 3-1. \uD604\uC7AC\uCE78 \uB9C8\uC9C0\uB9C9 \uBB38\uC790\uC5D0\uC11C \uC624\uB978\uCABD\uD0A4 \u2192 \uB2E4\uC74C\uCE78 \uCCAB\uBC88\uC9F8\uB85C\r\n if (\r\n selectionEnd >= group.value.length - 1 &&\r\n group.value.length > 0 &&\r\n groupIndex < groups.length - 1\r\n ) {\r\n e.preventDefault();\r\n const nextGroup = groups[groupIndex + 1];\r\n isArrowFocusRef.current = true;\r\n arrowTargetPosRef.current = 0;\r\n nextGroup.ref.current?.focus();\r\n nextGroup.ref.current?.setSelectionRange(0, 0);\r\n } else if (selectionEnd < group.value.length - 1) {\r\n // \uB9C8\uC9C0\uB9C9 \uBB38\uC790 \uC804\uAE4C\uC9C0\uB9CC \uC624\uB978\uCABD \uC774\uB3D9\r\n e.preventDefault();\r\n const newPos = selectionEnd + 1;\r\n input?.setSelectionRange(newPos, newPos);\r\n cursorPosRef.current = newPos;\r\n setCursorVisible(true);\r\n setRenderTrigger((prev) => prev + 1);\r\n }\r\n } else if (e.key === \"Backspace\") {\r\n // \uC804\uCCB4 \uC120\uD0DD \uC0C1\uD0DC\uC5D0\uC11C\uB294 \uC804\uCCB4 \uC0AD\uC81C\r\n if (hasSelection) {\r\n e.preventDefault();\r\n const newValue =\r\n group.value.slice(0, selectionStart) +\r\n group.value.slice(selectionEnd);\r\n group.setValue(newValue);\r\n cursorPosRef.current = selectionStart;\r\n setCursorVisible(true);\r\n setInputComplete(false);\r\n setRenderTrigger((prev) => prev + 1);\r\n setTimeout(() => {\r\n input?.setSelectionRange(\r\n selectionStart,\r\n selectionStart\r\n );\r\n }, 0);\r\n onValueChange?.(newValue, groupIndex);\r\n } else if (selectionStart > 0) {\r\n // 1-3. \uD604\uC7AC\uCEE4\uC11C \uC55E\uC790\uB9AC \uC0AD\uC81C\r\n e.preventDefault();\r\n const newValue =\r\n group.value.slice(0, selectionStart - 1) +\r\n group.value.slice(selectionStart);\r\n group.setValue(newValue);\r\n const newPos = selectionStart - 1;\r\n cursorPosRef.current = newPos;\r\n setCursorVisible(true);\r\n setInputComplete(false);\r\n setRenderTrigger((prev) => prev + 1);\r\n setTimeout(() => {\r\n input?.setSelectionRange(newPos, newPos);\r\n }, 0);\r\n onValueChange?.(newValue, groupIndex);\r\n } else if (groupIndex > 0) {\r\n // 1-4. \uD604\uC7AC\uCE78 \uC81C\uC77C \uC55E\uC5D0\uC11C \u2192 \uC774\uC804\uCE78 \uB9C8\uC9C0\uB9C9 \uC790\uB9AC\uAC12 \uC0AD\uC81C \uB610\uB294 \uCEE4\uC11C \uC774\uB3D9\r\n e.preventDefault();\r\n const prevGroup = groups[groupIndex - 1];\r\n const prevLen = prevGroup.value.length;\r\n\r\n if (prevLen > 0) {\r\n // \uC774\uC804 \uCE78\uC5D0 \uAC12\uC774 \uC788\uC73C\uBA74 \uB9C8\uC9C0\uB9C9 \uAC12 \uC0AD\uC81C\r\n const newPrevValue = prevGroup.value.slice(0, -1);\r\n prevGroup.setValue(newPrevValue);\r\n cursorPosRef.current = newPrevValue.length;\r\n setInputComplete(false);\r\n onValueChange?.(newPrevValue, groupIndex - 1);\r\n } else {\r\n // \uC774\uC804 \uCE78\uC774 \uBE44\uC5B4\uC788\uC73C\uBA74 \uCEE4\uC11C\uB9CC \uC774\uB3D9\r\n cursorPosRef.current = 0;\r\n }\r\n\r\n setCursorVisible(true);\r\n setFocusedGroup(groupIndex - 1);\r\n setRenderTrigger((prev) => prev + 1);\r\n prevGroup.ref.current?.focus();\r\n const finalPos = prevLen > 0 ? prevLen - 1 : 0;\r\n prevGroup.ref.current?.setSelectionRange(\r\n finalPos,\r\n finalPos\r\n );\r\n }\r\n } else if (e.key === \"Delete\") {\r\n // \uC804\uCCB4 \uC120\uD0DD \uC0C1\uD0DC\uC5D0\uC11C\uB294 \uC804\uCCB4 \uC0AD\uC81C\r\n if (hasSelection) {\r\n e.preventDefault();\r\n const newValue =\r\n group.value.slice(0, selectionStart) +\r\n group.value.slice(selectionEnd);\r\n group.setValue(newValue);\r\n cursorPosRef.current = selectionStart;\r\n setCursorVisible(true);\r\n setInputComplete(false);\r\n setRenderTrigger((prev) => prev + 1);\r\n setTimeout(() => {\r\n input?.setSelectionRange(\r\n selectionStart,\r\n selectionStart\r\n );\r\n }, 0);\r\n onValueChange?.(newValue, groupIndex);\r\n } else if (selectionStart < group.value.length) {\r\n // 1-5. del \uD0A4\uB85C \uD604\uC7AC \uCEE4\uC11C \uC704\uCE58 \uAC12 \uC0AD\uC81C\r\n e.preventDefault();\r\n const newValue =\r\n group.value.slice(0, selectionStart) +\r\n group.value.slice(selectionStart + 1);\r\n group.setValue(newValue);\r\n cursorPosRef.current = selectionStart;\r\n setCursorVisible(true);\r\n setInputComplete(false);\r\n setRenderTrigger((prev) => prev + 1);\r\n setTimeout(() => {\r\n input?.setSelectionRange(\r\n selectionStart,\r\n selectionStart\r\n );\r\n }, 0);\r\n onValueChange?.(newValue, groupIndex);\r\n }\r\n }\r\n };\r\n },\r\n [groups]\r\n );\r\n\r\n // Change \uD578\uB4E4\uB7EC \uC0DD\uC131\r\n const createChangeHandler = useCallback(\r\n (\r\n groupIndex: number,\r\n onAfterChange?: (\r\n newValue: string,\r\n groupIndex: number,\r\n isComplete: boolean\r\n ) => void\r\n ) => {\r\n return (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const group = groups[groupIndex];\r\n const numbers = e.target.value\r\n .replace(/\\D/g, \"\")\r\n .slice(0, group.maxLength);\r\n group.setValue(numbers);\r\n cursorPosRef.current = numbers.length;\r\n setCursorVisible(true); // \uC785\uB825 \uC2DC \uCEE4\uC11C \uD45C\uC2DC\r\n\r\n // \uCD5C\uB300 \uAE38\uC774 \uC785\uB825 \uC2DC \uB2E4\uC74C \uADF8\uB8F9\uC73C\uB85C \uC774\uB3D9\r\n const isGroupComplete = numbers.length === group.maxLength;\r\n if (isGroupComplete && groupIndex < groups.length - 1) {\r\n setTimeout(() => {\r\n const nextGroup = groups[groupIndex + 1];\r\n nextGroup.ref.current?.focus();\r\n nextGroup.ref.current?.setSelectionRange(0, 0);\r\n cursorPosRef.current = 0;\r\n }, 0);\r\n }\r\n\r\n // \uC804\uCCB4 \uC644\uB8CC \uCCB4\uD06C\r\n const allComplete =\r\n isGroupComplete &&\r\n groupIndex === groups.length - 1 &&\r\n groups\r\n .slice(0, -1)\r\n .every((g) => g.value.length === g.maxLength);\r\n\r\n if (allComplete) {\r\n setInputComplete(true);\r\n onComplete?.();\r\n } else {\r\n setInputComplete(false);\r\n }\r\n\r\n setRenderTrigger((prev) => prev + 1);\r\n onAfterChange?.(numbers, groupIndex, allComplete);\r\n };\r\n },\r\n [groups, onComplete]\r\n );\r\n\r\n return {\r\n focusedGroup,\r\n setFocusedGroup,\r\n cursorVisible,\r\n setCursorVisible,\r\n cursorPosRef,\r\n isClickFocusRef,\r\n inputComplete,\r\n setInputComplete,\r\n renderTrigger,\r\n measureTextWidth,\r\n getCursorLeft,\r\n createMouseDownHandler,\r\n createClickHandler,\r\n createContainerClickHandler,\r\n createFocusHandler,\r\n createBlurHandler,\r\n createKeyDownHandler,\r\n createChangeHandler,\r\n getCursorPosFromClick,\r\n forceRender,\r\n };\r\n}\r\n"],
4
+ "sourcesContent": ["export { AddressTextField } from \"./AddressTextField\";\r\nexport type { AddressTextFieldProps, DaumPostcodeData } from \"./types\";\r\n", "/**\r\n * AddressTextField.tsx\r\n *\r\n * @license MIT\r\n form,\r\n * @copyright 2025 \uAE40\uC601\uC9C4 (Kim Young Jin)\r\n * @author \uAE40\uC601\uC9C4 (ehfuse@gmail.com)\r\n */\r\n\r\nimport {\r\n IconButton,\r\n InputAdornment,\r\n TextField,\r\n Dialog,\r\n DialogContent,\r\n DialogTitle,\r\n} from \"@mui/material\";\r\nimport SearchIcon from \"@mui/icons-material/Search\";\r\nimport CloseIcon from \"@mui/icons-material/Close\";\r\nimport {\r\n useState,\r\n useEffect,\r\n type ChangeEvent,\r\n useCallback,\r\n forwardRef,\r\n useRef,\r\n useImperativeHandle,\r\n} from \"react\";\r\nimport DaumPostcode from \"react-daum-postcode\";\r\nimport type { AddressTextFieldProps, DaumPostcodeData } from \"./types\";\r\nimport { useDebouncedEmit } from \"./hooks\";\r\n\r\nconst AddressTextFieldBase = forwardRef<HTMLDivElement, AddressTextFieldProps>(\r\n function AddressTextFieldBase(\r\n {\r\n value,\r\n onChange,\r\n readonly,\r\n debounce,\r\n onBlur,\r\n size,\r\n spellCheck = false,\r\n inputRef: externalInputRef,\r\n ...props\r\n },\r\n ref,\r\n ) {\r\n const internalInputRef = useRef<HTMLInputElement>(null);\r\n\r\n // \uC678\uBD80 inputRef\uC640 \uB0B4\uBD80 inputRef \uBCD1\uD569\r\n useImperativeHandle(\r\n externalInputRef as React.Ref<HTMLInputElement>,\r\n () => internalInputRef.current!,\r\n [],\r\n );\r\n\r\n const [address, setAddress] = useState<string>(\r\n typeof value === \"string\" ? value : \"\",\r\n );\r\n const [isPostcodeOpen, setIsPostcodeOpen] = useState(false);\r\n\r\n // size\uC5D0 \uB530\uB978 \uC544\uC774\uCF58 \uD06C\uAE30\r\n const iconSize = size === \"small\" ? 20 : 24;\r\n\r\n // useDebouncedEmit \uD6C5 \uC0AC\uC6A9\r\n const { emitChange, flushOnBlur, lastEmittedValue } = useDebouncedEmit({\r\n name: props.name,\r\n debounce,\r\n onChange,\r\n });\r\n\r\n // value prop\uC774 \uBCC0\uACBD\uB420 \uB54C \uB0B4\uBD80 state \uC5C5\uB370\uC774\uD2B8 (\uC678\uBD80\uC5D0\uC11C \uBCC0\uACBD\uB41C \uACBD\uC6B0\uB9CC)\r\n useEffect(() => {\r\n if (value !== undefined && value !== null) {\r\n const strValue = String(value);\r\n // \uC678\uBD80\uC5D0\uC11C \uBCC0\uACBD\uB41C \uAC12\uC774 \uB9C8\uC9C0\uB9C9\uC73C\uB85C emit\uD55C \uAC12\uACFC \uB2E4\uB97C \uB54C\uB9CC \uB3D9\uAE30\uD654\r\n if (strValue !== lastEmittedValue.current) {\r\n setAddress(strValue);\r\n lastEmittedValue.current = strValue;\r\n }\r\n } else if (lastEmittedValue.current !== \"\") {\r\n setAddress(\"\");\r\n lastEmittedValue.current = \"\";\r\n }\r\n }, [value, lastEmittedValue]);\r\n\r\n const handlePostcodeComplete = (data: DaumPostcodeData) => {\r\n let fullAddress = data.address;\r\n let extraAddress = \"\";\r\n\r\n // \uC0AC\uC6A9\uC790\uAC00 \uC120\uD0DD\uD55C \uC8FC\uC18C \uD0C0\uC785\uC5D0 \uB530\uB77C \uD574\uB2F9 \uC8FC\uC18C \uAC12\uC744 \uAC00\uC838\uC628\uB2E4.\r\n if (data.addressType === \"R\") {\r\n // \uB3C4\uB85C\uBA85 \uC8FC\uC18C\uB97C \uC120\uD0DD\uD588\uC744 \uACBD\uC6B0\r\n if (data.bname !== \"\" && /[\uB3D9|\uB85C|\uAC00]$/g.test(data.bname)) {\r\n extraAddress += data.bname;\r\n }\r\n if (data.buildingName !== \"\" && data.apartment === \"Y\") {\r\n extraAddress +=\r\n extraAddress !== \"\"\r\n ? \", \" + data.buildingName\r\n : data.buildingName;\r\n }\r\n if (extraAddress !== \"\") {\r\n extraAddress = \" (\" + extraAddress + \")\";\r\n }\r\n fullAddress += extraAddress;\r\n }\r\n\r\n // \uC8FC\uC18C \uC124\uC815\r\n setAddress(fullAddress);\r\n\r\n // \uC8FC\uC18C \uBCC0\uACBD \uC774\uBCA4\uD2B8 \uBC1C\uC0DD (\uD31D\uC5C5 \uC120\uD0DD\uC740 \uC989\uC2DC \uD638\uCD9C)\r\n emitChange(null, fullAddress, true);\r\n\r\n // \uD31D\uC5C5 \uB2EB\uAE30\r\n setIsPostcodeOpen(false);\r\n };\r\n\r\n const handleSearchButtonClick = () => {\r\n setIsPostcodeOpen(true);\r\n };\r\n\r\n const handleKeyDown = (\r\n event: React.KeyboardEvent<HTMLInputElement>,\r\n ) => {\r\n if (event.key === \"Enter\") {\r\n event.preventDefault();\r\n handleSearchButtonClick();\r\n }\r\n };\r\n\r\n const handlePostcodeClose = () => {\r\n setIsPostcodeOpen(false);\r\n };\r\n\r\n const onClose = () => {\r\n setIsPostcodeOpen(false);\r\n emitChange(null, address as string, true);\r\n };\r\n\r\n // blur \uC2DC \uB300\uAE30 \uC911\uC778 \uB514\uBC14\uC6B4\uC2A4 \uC989\uC2DC \uC2E4\uD589\r\n const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {\r\n flushOnBlur(e, address as string);\r\n if (onBlur) {\r\n onBlur(e);\r\n }\r\n };\r\n\r\n // readonly\uC77C \uB54C \uD3EC\uCEE4\uC2A4 \uC2A4\uD0C0\uC77C \uC81C\uAC70\r\n const readonlyStyles = readonly\r\n ? {\r\n \"& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline\":\r\n {\r\n borderColor: \"rgba(0, 0, 0, 0.23)\",\r\n borderWidth: 1,\r\n },\r\n \"& .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline\":\r\n {\r\n borderColor: \"rgba(0, 0, 0, 0.23)\",\r\n },\r\n \"& .MuiInputLabel-root.Mui-focused\": {\r\n color: \"rgba(0, 0, 0, 0.6)\",\r\n },\r\n }\r\n : {};\r\n\r\n return (\r\n <>\r\n <TextField\r\n {...props}\r\n ref={ref}\r\n inputRef={internalInputRef}\r\n size={size}\r\n value={address}\r\n onChange={\r\n readonly\r\n ? undefined\r\n : (e) => {\r\n const newValue = e.target.value;\r\n setAddress(newValue);\r\n emitChange(\r\n e as ChangeEvent<HTMLInputElement>,\r\n newValue,\r\n );\r\n }\r\n }\r\n autoComplete=\"off\"\r\n spellCheck={spellCheck}\r\n onKeyDown={readonly ? undefined : handleKeyDown}\r\n onBlur={readonly ? undefined : handleBlur}\r\n sx={{\r\n ...readonlyStyles,\r\n ...(readonly && { pointerEvents: \"none\" }),\r\n }}\r\n slotProps={{\r\n ...props.slotProps,\r\n input: {\r\n ...props.slotProps?.input,\r\n readOnly: readonly,\r\n endAdornment: readonly ? undefined : (\r\n <InputAdornment\r\n position=\"end\"\r\n sx={{ mr: size === \"small\" ? -0.5 : 0.5 }}\r\n >\r\n <IconButton\r\n onClick={handleSearchButtonClick}\r\n edge=\"end\"\r\n aria-label=\"\uC8FC\uC18C \uCC3E\uAE30\"\r\n size={size}\r\n >\r\n <SearchIcon\r\n sx={{ fontSize: iconSize }}\r\n />\r\n </IconButton>\r\n </InputAdornment>\r\n ),\r\n },\r\n }}\r\n />\r\n\r\n <Dialog\r\n open={isPostcodeOpen}\r\n onClose={handlePostcodeClose}\r\n maxWidth=\"sm\"\r\n fullWidth\r\n slotProps={{\r\n paper: {\r\n sx: { height: \"550px\" },\r\n },\r\n }}\r\n >\r\n <DialogTitle\r\n sx={{\r\n display: \"flex\",\r\n justifyContent: \"space-between\",\r\n alignItems: \"center\",\r\n pr: 2,\r\n }}\r\n >\r\n \uC8FC\uC18C \uAC80\uC0C9\r\n <IconButton onClick={onClose} size=\"small\">\r\n <CloseIcon />\r\n </IconButton>\r\n </DialogTitle>\r\n <DialogContent style={{ padding: 0 }} dividers>\r\n <DaumPostcode\r\n onComplete={handlePostcodeComplete}\r\n onClose={handlePostcodeClose}\r\n defaultQuery={address as string}\r\n style={{\r\n width: \"100%\",\r\n height: \"100%\",\r\n }}\r\n />\r\n </DialogContent>\r\n </Dialog>\r\n </>\r\n );\r\n },\r\n);\r\n\r\nconst AddressTextFieldWithForm = forwardRef<\r\n HTMLDivElement,\r\n AddressTextFieldProps\r\n>(function AddressTextFieldWithForm({ form, name, onChange, ...rest }, ref) {\r\n if (!form || typeof form.useFormValue !== \"function\") {\r\n throw new Error(\r\n \"AddressTextField form prop is missing. Provide a form from useForm or useGlobalForm.\",\r\n );\r\n }\r\n if (!name) {\r\n throw new Error(\r\n \"AddressTextField requires a name when form prop is provided.\",\r\n );\r\n }\r\n\r\n const formValue = form.useFormValue(name);\r\n const handleFormChange = useCallback(\r\n (event: React.ChangeEvent<HTMLInputElement>) => {\r\n form.handleFormChange(event);\r\n onChange?.(event);\r\n },\r\n [form, onChange],\r\n );\r\n\r\n return (\r\n <AddressTextFieldBase\r\n {...rest}\r\n ref={ref}\r\n name={name}\r\n value={formValue}\r\n onChange={handleFormChange}\r\n />\r\n );\r\n});\r\n\r\nexport const AddressTextField = forwardRef<\r\n HTMLDivElement,\r\n AddressTextFieldProps\r\n>(function AddressTextField(props, ref) {\r\n if (props.form) {\r\n return <AddressTextFieldWithForm {...props} ref={ref} />;\r\n }\r\n\r\n return <AddressTextFieldBase {...props} ref={ref} />;\r\n});\r\n", "/**\r\n * useTextFieldBase.ts\r\n *\r\n * TextField \uCEF4\uD3EC\uB10C\uD2B8\uB4E4\uC758 \uACF5\uD1B5 \uB85C\uC9C1\uC744 \uB2F4\uB2F9\uD558\uB294 \uD6C5\r\n * - \uB514\uBC14\uC6B4\uC2A4 \uCC98\uB9AC\r\n * - \uB0B4\uBD80 \uC0C1\uD0DC \uAD00\uB9AC\r\n * - blur \uC2DC flush\r\n * - \uC678\uBD80 value \uB3D9\uAE30\uD654\r\n *\r\n * @license MIT\r\n * @copyright 2025 \uAE40\uC601\uC9C4 (Kim Young Jin)\r\n * @author \uAE40\uC601\uC9C4 (ehfuse@gmail.com)\r\n */\r\n\r\nimport { useState, useRef, useEffect, useCallback, ChangeEvent } from \"react\";\r\n\r\nexport interface UseTextFieldBaseOptions {\r\n value: string; // \uC678\uBD80\uC5D0\uC11C \uC804\uB2EC\uBC1B\uC740 value\r\n name?: string; // \uD544\uB4DC \uC774\uB984 (name \uC18D\uC131)\r\n debounce?: number; // \uB514\uBC14\uC6B4\uC2A4 \uC9C0\uC5F0 \uC2DC\uAC04 (ms). undefined\uBA74 \uB514\uBC14\uC6B4\uC2A4 \uC5C6\uC74C\r\n onChange?: (e: ChangeEvent<HTMLInputElement>) => void; // \uAC12 \uBCC0\uACBD \uC2DC \uD638\uCD9C\uB418\uB294 \uCF5C\uBC31\r\n onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void; // blur \uC774\uBCA4\uD2B8 \uCF5C\uBC31\r\n // \uB0B4\uBD80 \uAC12\uC744 \uBCC0\uD658\uD558\uB294 \uD568\uC218 (\uC608: \uC804\uD654\uBC88\uD638 \uD3EC\uB9F7\uD305, \uC22B\uC790 \uD3EC\uB9F7\uD305 \uB4F1)\r\n transformValue?: (\r\n inputValue: string,\r\n currentDisplayValue: string,\r\n ) => {\r\n displayValue: string;\r\n emitValue: string;\r\n };\r\n}\r\n\r\nexport interface UseTextFieldBaseReturn {\r\n internalValue: string; // \uD654\uBA74\uC5D0 \uD45C\uC2DC\uD560 \uB0B4\uBD80 \uAC12\r\n setInternalValue: (value: string) => void; // \uB0B4\uBD80 \uAC12\uC744 \uC9C1\uC811 \uC124\uC815\r\n handleChange: (e: ChangeEvent<HTMLInputElement>) => void; // TextField onChange \uD578\uB4E4\uB7EC\r\n handleBlur: (e: React.FocusEvent<HTMLInputElement>) => void; // TextField onBlur \uD578\uB4E4\uB7EC\r\n emitChange: (\r\n // \uD2B9\uC815 \uAC12\uC73C\uB85C \uBCC0\uACBD\uC744 emit (immediate=true\uBA74 \uB514\uBC14\uC6B4\uC2A4 \uBB34\uC2DC)\r\n e: ChangeEvent<HTMLInputElement> | React.FocusEvent<HTMLInputElement>,\r\n newValue: string,\r\n immediate?: boolean,\r\n ) => void;\r\n lastEmittedValue: React.MutableRefObject<string>; // \uB9C8\uC9C0\uB9C9\uC73C\uB85C emit\uB41C \uAC12\r\n debounceTimer: React.MutableRefObject<ReturnType<typeof setTimeout> | null>; // \uB514\uBC14\uC6B4\uC2A4 \uD0C0\uC774\uBA38 ref\r\n}\r\n\r\nexport function useTextFieldBase({\r\n value,\r\n name = \"\",\r\n debounce,\r\n onChange,\r\n onBlur,\r\n transformValue,\r\n}: UseTextFieldBaseOptions): UseTextFieldBaseReturn {\r\n // \uD654\uBA74\uC5D0 \uD45C\uC2DC\uB418\uB294 \uB0B4\uBD80 \uAC12\r\n const [internalValue, setInternalValue] = useState<string>(value ?? \"\");\r\n\r\n // \uB9C8\uC9C0\uB9C9\uC73C\uB85C onChange\uAC00 \uD638\uCD9C\uB41C \uAC12 (\uC911\uBCF5 \uD638\uCD9C \uBC29\uC9C0)\r\n const lastEmittedValue = useRef<string>(value ?? \"\");\r\n\r\n // \uB514\uBC14\uC6B4\uC2A4 \uD0C0\uC774\uBA38\r\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n\r\n // \uC774\uC804 \uC678\uBD80 value (\uC678\uBD80\uC5D0\uC11C \uBCC0\uACBD\uB41C \uAC83\uC778\uC9C0 \uAC10\uC9C0)\r\n const prevExternalValue = useRef<string>(value ?? \"\");\r\n\r\n // \uCEF4\uD3EC\uB10C\uD2B8 \uC5B8\uB9C8\uC6B4\uD2B8 \uC2DC \uD0C0\uC774\uBA38 \uC815\uB9AC\r\n useEffect(() => {\r\n return () => {\r\n if (debounceTimer.current) {\r\n clearTimeout(debounceTimer.current);\r\n }\r\n };\r\n }, []);\r\n\r\n // \uC678\uBD80 value prop \uBCC0\uACBD \uC2DC \uB0B4\uBD80 \uC0C1\uD0DC \uB3D9\uAE30\uD654\r\n useEffect(() => {\r\n const newValue = value ?? \"\";\r\n // \uC678\uBD80\uC5D0\uC11C \uAC12\uC774 \uBCC0\uACBD\uB418\uC5C8\uACE0, \uADF8 \uAC12\uC774 \uC6B0\uB9AC\uAC00 \uB9C8\uC9C0\uB9C9\uC73C\uB85C emit\uD55C \uAC12\uACFC \uB2E4\uB974\uBA74 \uB3D9\uAE30\uD654\r\n if (\r\n newValue !== prevExternalValue.current &&\r\n newValue !== lastEmittedValue.current\r\n ) {\r\n setInternalValue(newValue);\r\n lastEmittedValue.current = newValue;\r\n }\r\n prevExternalValue.current = newValue;\r\n }, [value]);\r\n\r\n // onChange\uB97C emit\uD558\uB294 \uD568\uC218\r\n const emitChange = useCallback(\r\n (\r\n e:\r\n | ChangeEvent<HTMLInputElement>\r\n | React.FocusEvent<HTMLInputElement>,\r\n newValue: string,\r\n immediate = false,\r\n ) => {\r\n // \uAC19\uC740 \uAC12\uC774\uBA74 \uD638\uCD9C\uD558\uC9C0 \uC54A\uC74C\r\n if (newValue === lastEmittedValue.current) {\r\n return;\r\n }\r\n\r\n const doEmit = () => {\r\n if (newValue !== lastEmittedValue.current) {\r\n lastEmittedValue.current = newValue;\r\n if (onChange) {\r\n const syntheticEvent = {\r\n ...e,\r\n target: {\r\n ...e.target,\r\n name: name,\r\n value: newValue,\r\n },\r\n } as ChangeEvent<HTMLInputElement>;\r\n onChange(syntheticEvent);\r\n }\r\n }\r\n };\r\n\r\n if (debounceTimer.current) {\r\n clearTimeout(debounceTimer.current);\r\n }\r\n\r\n if (immediate || debounce === undefined || debounce === 0) {\r\n doEmit();\r\n } else {\r\n debounceTimer.current = setTimeout(doEmit, debounce);\r\n }\r\n },\r\n [onChange, debounce, name],\r\n );\r\n\r\n // TextField onChange \uD578\uB4E4\uB7EC\r\n const handleChange = useCallback(\r\n (e: ChangeEvent<HTMLInputElement>) => {\r\n const inputValue = e.target.value;\r\n\r\n if (transformValue) {\r\n // \uBCC0\uD658 \uD568\uC218\uAC00 \uC788\uC73C\uBA74 \uC0AC\uC6A9\r\n const { displayValue, emitValue } = transformValue(\r\n inputValue,\r\n internalValue,\r\n );\r\n setInternalValue(displayValue);\r\n emitChange(e, emitValue);\r\n } else {\r\n // \uBCC0\uD658 \uC5C6\uC774 \uADF8\uB300\uB85C \uC0AC\uC6A9\r\n setInternalValue(inputValue);\r\n emitChange(e, inputValue);\r\n }\r\n },\r\n [transformValue, internalValue, emitChange],\r\n );\r\n\r\n // blur \uC2DC \uB300\uAE30 \uC911\uC778 \uB514\uBC14\uC6B4\uC2A4 \uC989\uC2DC \uC2E4\uD589\r\n const handleBlur = useCallback(\r\n (e: React.FocusEvent<HTMLInputElement>) => {\r\n if (debounceTimer.current) {\r\n clearTimeout(debounceTimer.current);\r\n debounceTimer.current = null;\r\n\r\n // \uD604\uC7AC \uB0B4\uBD80 \uAC12\uC73C\uB85C \uC989\uC2DC emit\r\n if (internalValue !== lastEmittedValue.current) {\r\n lastEmittedValue.current = internalValue;\r\n if (onChange) {\r\n const syntheticEvent = {\r\n ...e,\r\n target: {\r\n ...e.target,\r\n name: name,\r\n value: internalValue,\r\n },\r\n } as ChangeEvent<HTMLInputElement>;\r\n onChange(syntheticEvent);\r\n }\r\n }\r\n }\r\n\r\n // \uC6D0\uB798 onBlur \uD638\uCD9C\r\n if (onBlur) {\r\n onBlur(e);\r\n }\r\n },\r\n [internalValue, onChange, onBlur, name],\r\n );\r\n\r\n return {\r\n internalValue,\r\n setInternalValue,\r\n handleChange,\r\n handleBlur,\r\n emitChange,\r\n lastEmittedValue,\r\n debounceTimer,\r\n };\r\n}\r\n\r\n// \uB514\uBC14\uC6B4\uC2A4\uB41C onChange emit\uB9CC \uB2F4\uB2F9\uD558\uB294 \uAC04\uB2E8\uD55C \uD6C5 (\uBCF5\uC7A1\uD55C \uD3EC\uB9F7\uD305 \uB85C\uC9C1\uC774 \uC788\uB294 \uCEF4\uD3EC\uB10C\uD2B8\uC5D0\uC11C \uC0AC\uC6A9)\r\nexport interface UseDebouncedEmitOptions {\r\n name?: string; // \uD544\uB4DC \uC774\uB984 (name \uC18D\uC131)\r\n debounce?: number; // \uB514\uBC14\uC6B4\uC2A4 \uC9C0\uC5F0 \uC2DC\uAC04 (ms). undefined\uBA74 \uB514\uBC14\uC6B4\uC2A4 \uC5C6\uC74C\r\n onChange?: (e: ChangeEvent<HTMLInputElement>) => void; // \uAC12 \uBCC0\uACBD \uC2DC \uD638\uCD9C\uB418\uB294 \uCF5C\uBC31\r\n}\r\n\r\nexport interface UseDebouncedEmitReturn {\r\n emitChange: (\r\n // \uB514\uBC14\uC6B4\uC2A4\uB41C onChange emit\r\n e:\r\n | ChangeEvent<HTMLInputElement>\r\n | React.FocusEvent<HTMLInputElement>\r\n | null,\r\n newValue: string,\r\n immediate?: boolean,\r\n ) => void;\r\n flushOnBlur: (\r\n // blur \uC2DC \uB300\uAE30 \uC911\uC778 \uB514\uBC14\uC6B4\uC2A4 flush\r\n e: React.FocusEvent<HTMLInputElement>,\r\n currentValue: string,\r\n ) => void;\r\n lastEmittedValue: React.MutableRefObject<string>; // \uB9C8\uC9C0\uB9C9\uC73C\uB85C emit\uB41C \uAC12\r\n debounceTimer: React.MutableRefObject<ReturnType<typeof setTimeout> | null>; // \uB514\uBC14\uC6B4\uC2A4 \uD0C0\uC774\uBA38 ref\r\n}\r\n\r\nexport function useDebouncedEmit({\r\n name = \"\",\r\n debounce,\r\n onChange,\r\n}: UseDebouncedEmitOptions): UseDebouncedEmitReturn {\r\n // \uB9C8\uC9C0\uB9C9\uC73C\uB85C onChange\uAC00 \uD638\uCD9C\uB41C \uAC12 (\uC911\uBCF5 \uD638\uCD9C \uBC29\uC9C0)\r\n const lastEmittedValue = useRef<string>(\"\");\r\n\r\n // \uB514\uBC14\uC6B4\uC2A4 \uD0C0\uC774\uBA38\r\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n\r\n // \uCEF4\uD3EC\uB10C\uD2B8 \uC5B8\uB9C8\uC6B4\uD2B8 \uC2DC \uD0C0\uC774\uBA38 \uC815\uB9AC\r\n useEffect(() => {\r\n return () => {\r\n if (debounceTimer.current) {\r\n clearTimeout(debounceTimer.current);\r\n }\r\n };\r\n }, []);\r\n\r\n // onChange\uB97C emit\uD558\uB294 \uD568\uC218\r\n const emitChange = useCallback(\r\n (\r\n e:\r\n | ChangeEvent<HTMLInputElement>\r\n | React.FocusEvent<HTMLInputElement>\r\n | null,\r\n newValue: string,\r\n immediate = false,\r\n ) => {\r\n // \uAC19\uC740 \uAC12\uC774\uBA74 \uD638\uCD9C\uD558\uC9C0 \uC54A\uC74C\r\n if (newValue === lastEmittedValue.current) {\r\n return;\r\n }\r\n\r\n const doEmit = () => {\r\n if (newValue !== lastEmittedValue.current) {\r\n lastEmittedValue.current = newValue;\r\n if (onChange) {\r\n const syntheticEvent = {\r\n ...(e || {}),\r\n target: {\r\n ...(e?.target || {}),\r\n name: name,\r\n value: newValue,\r\n },\r\n } as ChangeEvent<HTMLInputElement>;\r\n onChange(syntheticEvent);\r\n }\r\n }\r\n };\r\n\r\n if (debounceTimer.current) {\r\n clearTimeout(debounceTimer.current);\r\n }\r\n\r\n if (immediate || debounce === undefined || debounce === 0) {\r\n doEmit();\r\n } else {\r\n debounceTimer.current = setTimeout(doEmit, debounce);\r\n }\r\n },\r\n [onChange, debounce, name],\r\n );\r\n\r\n // blur \uC2DC \uB300\uAE30 \uC911\uC778 \uB514\uBC14\uC6B4\uC2A4 flush\r\n const flushOnBlur = useCallback(\r\n (e: React.FocusEvent<HTMLInputElement>, currentValue: string) => {\r\n if (debounceTimer.current) {\r\n clearTimeout(debounceTimer.current);\r\n debounceTimer.current = null;\r\n\r\n if (currentValue !== lastEmittedValue.current) {\r\n lastEmittedValue.current = currentValue;\r\n if (onChange) {\r\n const syntheticEvent = {\r\n ...e,\r\n target: {\r\n ...e.target,\r\n name: name,\r\n value: currentValue,\r\n },\r\n } as ChangeEvent<HTMLInputElement>;\r\n onChange(syntheticEvent);\r\n }\r\n }\r\n }\r\n },\r\n [onChange, name],\r\n );\r\n\r\n return {\r\n emitChange,\r\n flushOnBlur,\r\n lastEmittedValue,\r\n debounceTimer,\r\n };\r\n}\r\n", "/**\r\n * useKoreanHolidays.ts\r\n *\r\n * \uD55C\uAD6D\uCC9C\uBB38\uC5F0\uAD6C\uC6D0 \uD2B9\uC77C \uC815\uBCF4 API\uB97C \uC774\uC6A9\uD558\uC5EC \uACF5\uD734\uC77C\uC744 \uC870\uD68C\uD558\uB294 \uCEE4\uC2A4\uD140 \uD6C5\r\n *\r\n * @license MIT\r\n * @copyright 2025 \uAE40\uC601\uC9C4 (Kim Young Jin)\r\n * @author \uAE40\uC601\uC9C4 (ehfuse@gmail.com)\r\n */\r\n\r\nimport { useState, useEffect, useCallback } from \"react\";\r\n\r\n// \uB85C\uCEEC\uC2A4\uD1A0\uB9AC\uC9C0 \uD0A4 prefix\r\nconst STORAGE_KEY_PREFIX = \"korean-holidays-\";\r\n\r\n// API \uC751\uB2F5 \uD0C0\uC785\r\ninterface HolidayItem {\r\n dateKind: string;\r\n dateName: string;\r\n isHoliday: string;\r\n locdate: number;\r\n seq: number;\r\n}\r\n\r\ninterface ApiResponse {\r\n response: {\r\n header: {\r\n resultCode: string;\r\n resultMsg: string;\r\n };\r\n body: {\r\n items: {\r\n item: HolidayItem | HolidayItem[];\r\n };\r\n numOfRows: number;\r\n pageNo: number;\r\n totalCount: number;\r\n };\r\n };\r\n}\r\n\r\n// \uCE90\uC2DC\uB41C \uACF5\uD734\uC77C \uB370\uC774\uD130 \uD0C0\uC785\r\ninterface CachedHolidays {\r\n year: number;\r\n holidays: string[]; // YYYYMMDD \uD615\uC2DD\r\n fetchedAt: number; // timestamp\r\n}\r\n\r\n/**\r\n * \uD55C\uAD6D \uACF5\uD734\uC77C\uC744 \uC870\uD68C\uD558\uB294 \uCEE4\uC2A4\uD140 \uD6C5\r\n *\r\n * @param apiKey - \uACF5\uACF5\uB370\uC774\uD130\uD3EC\uD138 API \uC778\uC99D\uD0A4 (Encoding)\r\n * @param year - \uC870\uD68C\uD560 \uC5F0\uB3C4 (\uAE30\uBCF8\uAC12: \uD604\uC7AC \uC5F0\uB3C4)\r\n * @returns { holidays: Date[], loading: boolean, error: string | null }\r\n */\r\nexport function useKoreanHolidays(apiKey?: string, year?: number) {\r\n const targetYear = year ?? new Date().getFullYear();\r\n const [holidays, setHolidays] = useState<Date[]>([]);\r\n const [loading, setLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n // \uB85C\uCEEC\uC2A4\uD1A0\uB9AC\uC9C0\uC5D0\uC11C \uCE90\uC2DC\uB41C \uB370\uC774\uD130 \uAC00\uC838\uC624\uAE30\r\n const getCachedHolidays = useCallback((year: number): Date[] | null => {\r\n try {\r\n const cached = localStorage.getItem(`${STORAGE_KEY_PREFIX}${year}`);\r\n if (!cached) return null;\r\n\r\n const data: CachedHolidays = JSON.parse(cached);\r\n\r\n // 1\uB144 \uC774\uC0C1 \uB41C \uCE90\uC2DC\uB294 \uBB34\uD6A8\uD654 (\uB2E4\uC74C \uD574\uC5D0 \uB2E4\uC2DC \uC870\uD68C)\r\n const oneYearMs = 365 * 24 * 60 * 60 * 1000;\r\n if (Date.now() - data.fetchedAt > oneYearMs) {\r\n localStorage.removeItem(`${STORAGE_KEY_PREFIX}${year}`);\r\n return null;\r\n }\r\n\r\n // YYYYMMDD \uBB38\uC790\uC5F4\uC744 Date \uAC1D\uCCB4\uB85C \uBCC0\uD658\r\n return data.holidays.map((dateStr) => {\r\n const y = parseInt(dateStr.slice(0, 4), 10);\r\n const m = parseInt(dateStr.slice(4, 6), 10) - 1;\r\n const d = parseInt(dateStr.slice(6, 8), 10);\r\n return new Date(y, m, d);\r\n });\r\n } catch {\r\n return null;\r\n }\r\n }, []);\r\n\r\n // \uB85C\uCEEC\uC2A4\uD1A0\uB9AC\uC9C0\uC5D0 \uCE90\uC2DC \uC800\uC7A5\r\n const setCachedHolidays = useCallback(\r\n (year: number, holidays: string[]) => {\r\n try {\r\n const data: CachedHolidays = {\r\n year,\r\n holidays,\r\n fetchedAt: Date.now(),\r\n };\r\n localStorage.setItem(\r\n `${STORAGE_KEY_PREFIX}${year}`,\r\n JSON.stringify(data)\r\n );\r\n } catch {\r\n // \uB85C\uCEEC\uC2A4\uD1A0\uB9AC\uC9C0 \uC6A9\uB7C9 \uCD08\uACFC \uB4F1\uC758 \uC5D0\uB7EC \uBB34\uC2DC\r\n }\r\n },\r\n []\r\n );\r\n\r\n // API\uC5D0\uC11C \uACF5\uD734\uC77C \uC870\uD68C\r\n const fetchHolidays = useCallback(\r\n async (year: number): Promise<Date[]> => {\r\n if (!apiKey) {\r\n throw new Error(\"API \uD0A4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.\");\r\n }\r\n\r\n const baseUrl =\r\n \"https://apis.data.go.kr/B090041/openapi/service/SpcdeInfoService/getRestDeInfo\";\r\n const params = new URLSearchParams({\r\n serviceKey: apiKey,\r\n solYear: String(year),\r\n numOfRows: \"100\",\r\n _type: \"json\",\r\n });\r\n\r\n const response = await fetch(`${baseUrl}?${params.toString()}`);\r\n\r\n if (!response.ok) {\r\n throw new Error(`API \uC694\uCCAD \uC2E4\uD328: ${response.status}`);\r\n }\r\n\r\n const data: ApiResponse = await response.json();\r\n\r\n if (data.response.header.resultCode !== \"00\") {\r\n throw new Error(`API \uC5D0\uB7EC: ${data.response.header.resultMsg}`);\r\n }\r\n\r\n const items = data.response.body.items?.item;\r\n if (!items) {\r\n return [];\r\n }\r\n\r\n // \uB2E8\uC77C \uD56D\uBAA9\uC77C \uACBD\uC6B0 \uBC30\uC5F4\uB85C \uBCC0\uD658\r\n const itemArray = Array.isArray(items) ? items : [items];\r\n\r\n // isHoliday\uAC00 \"Y\"\uC778 \uD56D\uBAA9\uB9CC \uD544\uD130\uB9C1\r\n const holidayDates = itemArray\r\n .filter((item) => item.isHoliday === \"Y\")\r\n .map((item) => {\r\n const dateStr = String(item.locdate);\r\n const y = parseInt(dateStr.slice(0, 4), 10);\r\n const m = parseInt(dateStr.slice(4, 6), 10) - 1;\r\n const d = parseInt(dateStr.slice(6, 8), 10);\r\n return new Date(y, m, d);\r\n });\r\n\r\n // \uCE90\uC2DC\uC5D0 \uC800\uC7A5 (YYYYMMDD \uD615\uC2DD\uC73C\uB85C)\r\n const holidayStrings = itemArray\r\n .filter((item) => item.isHoliday === \"Y\")\r\n .map((item) => String(item.locdate));\r\n setCachedHolidays(year, holidayStrings);\r\n\r\n return holidayDates;\r\n },\r\n [apiKey, setCachedHolidays]\r\n );\r\n\r\n useEffect(() => {\r\n if (!apiKey) {\r\n setHolidays([]);\r\n return;\r\n }\r\n\r\n // \uCE90\uC2DC\uB41C \uB370\uC774\uD130 \uD655\uC778\r\n const cached = getCachedHolidays(targetYear);\r\n if (cached) {\r\n setHolidays(cached);\r\n return;\r\n }\r\n\r\n // API \uD638\uCD9C\r\n setLoading(true);\r\n setError(null);\r\n\r\n fetchHolidays(targetYear)\r\n .then((dates) => {\r\n setHolidays(dates);\r\n })\r\n .catch((err) => {\r\n setError(err.message);\r\n setHolidays([]);\r\n })\r\n .finally(() => {\r\n setLoading(false);\r\n });\r\n }, [apiKey, targetYear, getCachedHolidays, fetchHolidays]);\r\n\r\n return { holidays, loading, error };\r\n}\r\n\r\n/**\r\n * \uC5EC\uB7EC \uC5F0\uB3C4\uC758 \uACF5\uD734\uC77C\uC744 \uD55C\uBC88\uC5D0 \uC870\uD68C\uD558\uB294 \uD6C5\r\n */\r\nexport function useKoreanHolidaysRange(\r\n apiKey?: string,\r\n startYear?: number,\r\n endYear?: number\r\n) {\r\n const currentYear = new Date().getFullYear();\r\n const start = startYear ?? currentYear;\r\n const end = endYear ?? currentYear;\r\n\r\n const [holidays, setHolidays] = useState<Date[]>([]);\r\n const [loading, setLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n useEffect(() => {\r\n if (!apiKey) {\r\n setHolidays([]);\r\n return;\r\n }\r\n\r\n const years: number[] = [];\r\n for (let y = start; y <= end; y++) {\r\n years.push(y);\r\n }\r\n\r\n setLoading(true);\r\n setError(null);\r\n\r\n // \uAC01 \uC5F0\uB3C4\uBCC4\uB85C \uCE90\uC2DC \uD655\uC778 \uB610\uB294 API \uD638\uCD9C\r\n Promise.all(\r\n years.map(async (year) => {\r\n const storageKey = `${STORAGE_KEY_PREFIX}${year}`;\r\n const cached = localStorage.getItem(storageKey);\r\n\r\n if (cached) {\r\n try {\r\n const data: CachedHolidays = JSON.parse(cached);\r\n return data.holidays.map((dateStr) => {\r\n const y = parseInt(dateStr.slice(0, 4), 10);\r\n const m = parseInt(dateStr.slice(4, 6), 10) - 1;\r\n const d = parseInt(dateStr.slice(6, 8), 10);\r\n return new Date(y, m, d);\r\n });\r\n } catch {\r\n // \uD30C\uC2F1 \uC2E4\uD328 \uC2DC API \uD638\uCD9C\r\n }\r\n }\r\n\r\n // API \uD638\uCD9C\r\n const baseUrl =\r\n \"https://apis.data.go.kr/B090041/openapi/service/SpcdeInfoService/getRestDeInfo\";\r\n const params = new URLSearchParams({\r\n serviceKey: apiKey,\r\n solYear: String(year),\r\n numOfRows: \"100\",\r\n _type: \"json\",\r\n });\r\n\r\n const response = await fetch(`${baseUrl}?${params.toString()}`);\r\n if (!response.ok) {\r\n throw new Error(`API \uC694\uCCAD \uC2E4\uD328: ${response.status}`);\r\n }\r\n\r\n const data: ApiResponse = await response.json();\r\n if (data.response.header.resultCode !== \"00\") {\r\n throw new Error(data.response.header.resultMsg);\r\n }\r\n\r\n const items = data.response.body.items?.item;\r\n if (!items) return [];\r\n\r\n const itemArray = Array.isArray(items) ? items : [items];\r\n const holidayStrings = itemArray\r\n .filter((item) => item.isHoliday === \"Y\")\r\n .map((item) => String(item.locdate));\r\n\r\n // \uCE90\uC2DC \uC800\uC7A5\r\n const cacheData: CachedHolidays = {\r\n year,\r\n holidays: holidayStrings,\r\n fetchedAt: Date.now(),\r\n };\r\n localStorage.setItem(storageKey, JSON.stringify(cacheData));\r\n\r\n return holidayStrings.map((dateStr) => {\r\n const y = parseInt(dateStr.slice(0, 4), 10);\r\n const m = parseInt(dateStr.slice(4, 6), 10) - 1;\r\n const d = parseInt(dateStr.slice(6, 8), 10);\r\n return new Date(y, m, d);\r\n });\r\n })\r\n )\r\n .then((results) => {\r\n const allHolidays = results.flat();\r\n setHolidays(allHolidays);\r\n })\r\n .catch((err) => {\r\n setError(err.message);\r\n setHolidays([]);\r\n })\r\n .finally(() => {\r\n setLoading(false);\r\n });\r\n }, [apiKey, start, end]);\r\n\r\n return { holidays, loading, error };\r\n}\r\n", "/**\r\n * useGroupedInput.ts\r\n *\r\n * \uADF8\uB8F9\uD654\uB41C \uC785\uB825 \uD544\uB4DC(\uC8FC\uBBFC\uBC88\uD638, \uC0AC\uC5C5\uC790\uBC88\uD638, \uC2DC\uAC04 \uB4F1)\uC5D0\uC11C \uACF5\uD1B5\uC73C\uB85C \uC0AC\uC6A9\uD558\uB294\r\n * \uCEE4\uC11C \uAD00\uB9AC, \uD3EC\uCEE4\uC2A4 \uAD00\uB9AC, \uD0A4\uBCF4\uB4DC \uD578\uB4E4\uB9C1 \uB85C\uC9C1\uC744 \uC81C\uACF5\uD558\uB294 \uD6C5\r\n *\r\n * @license MIT\r\n * @copyright 2025 \uAE40\uC601\uC9C4 (Kim Young Jin)\r\n * @author \uAE40\uC601\uC9C4 (ehfuse@gmail.com)\r\n */\r\n\r\nimport { useRef, useState, useCallback, useMemo, RefObject } from \"react\";\r\n\r\nexport interface GroupConfig {\r\n maxLength: number;\r\n ref: RefObject<HTMLInputElement | null>;\r\n value: string;\r\n setValue: (value: string) => void;\r\n}\r\n\r\nexport interface UseGroupedInputOptions {\r\n groups: GroupConfig[];\r\n fontFamily?: string;\r\n fontSize: number;\r\n onComplete?: () => void;\r\n disabled?: boolean;\r\n readonly?: boolean;\r\n}\r\n\r\nexport interface UseGroupedInputReturn {\r\n focusedGroup: number | null;\r\n setFocusedGroup: (group: number | null) => void;\r\n cursorVisible: boolean;\r\n setCursorVisible: (visible: boolean) => void;\r\n cursorPosRef: RefObject<number>;\r\n isClickFocusRef: RefObject<boolean>;\r\n inputComplete: boolean;\r\n setInputComplete: (complete: boolean) => void;\r\n renderTrigger: number;\r\n measureTextWidth: (text: string) => number;\r\n getCursorLeft: (groupIndex: number) => number;\r\n createMouseDownHandler: (\r\n groupIndex: number\r\n ) => (e: React.MouseEvent) => void;\r\n createClickHandler: (groupIndex: number) => (e: React.MouseEvent) => void;\r\n createContainerClickHandler: () => (e: React.MouseEvent) => void;\r\n createFocusHandler: (groupIndex: number) => () => void;\r\n createBlurHandler: () => () => void;\r\n createKeyDownHandler: (\r\n groupIndex: number,\r\n onValueChange?: (newValue: string, groupIndex: number) => void\r\n ) => (e: React.KeyboardEvent<HTMLInputElement>) => void;\r\n createChangeHandler: (\r\n groupIndex: number,\r\n onAfterChange?: (\r\n newValue: string,\r\n groupIndex: number,\r\n isComplete: boolean\r\n ) => void\r\n ) => (e: React.ChangeEvent<HTMLInputElement>) => void;\r\n getCursorPosFromClick: (\r\n e: React.MouseEvent,\r\n value: string,\r\n inputRef: RefObject<HTMLInputElement | null>\r\n ) => number;\r\n forceRender: () => void;\r\n}\r\n\r\nexport function useGroupedInput({\r\n groups,\r\n fontFamily,\r\n fontSize,\r\n onComplete,\r\n disabled = false,\r\n readonly = false,\r\n}: UseGroupedInputOptions): UseGroupedInputReturn {\r\n const [focusedGroup, setFocusedGroup] = useState<number | null>(null);\r\n const [cursorVisible, setCursorVisible] = useState(false);\r\n const [inputComplete, setInputComplete] = useState(false);\r\n const [renderTrigger, setRenderTrigger] = useState(0);\r\n\r\n const cursorPosRef = useRef(0);\r\n const isClickFocusRef = useRef(false);\r\n const isArrowFocusRef = useRef(false);\r\n const arrowTargetPosRef = useRef(0);\r\n\r\n // \uD14D\uC2A4\uD2B8 \uB108\uBE44 \uCE21\uC815\r\n const measureTextWidth = useCallback(\r\n (text: string): number => {\r\n if (typeof document === \"undefined\")\r\n return text.length * fontSize * 0.6;\r\n const canvas = document.createElement(\"canvas\");\r\n const ctx = canvas.getContext(\"2d\");\r\n if (!ctx) return text.length * fontSize * 0.6;\r\n ctx.font = `${fontSize}px ${fontFamily || \"Roboto, sans-serif\"}`;\r\n return ctx.measureText(text).width;\r\n },\r\n [fontSize, fontFamily]\r\n );\r\n\r\n // \uD074\uB9AD \uC704\uCE58\uC5D0\uC11C \uCEE4\uC11C \uC704\uCE58 \uACC4\uC0B0\r\n const getCursorPosFromClick = useCallback(\r\n (\r\n e: React.MouseEvent,\r\n value: string,\r\n inputRef: RefObject<HTMLInputElement | null>\r\n ): number => {\r\n const input = inputRef.current;\r\n if (!input) return value.length;\r\n\r\n const rect = input.getBoundingClientRect();\r\n const clickX = e.clientX - rect.left - 4; // padding \uBCF4\uC815\r\n\r\n let newPos = value.length;\r\n for (let i = 0; i <= value.length; i++) {\r\n const textWidth = measureTextWidth(value.slice(0, i));\r\n if (clickX < textWidth + measureTextWidth(\"0\") / 2) {\r\n newPos = i;\r\n break;\r\n }\r\n }\r\n return newPos;\r\n },\r\n [measureTextWidth]\r\n );\r\n\r\n // \uCEE4\uC11C \uC704\uCE58 \uACC4\uC0B0 (\uB80C\uB354\uB9C1\uC6A9)\r\n const getCursorLeft = useCallback(\r\n (groupIndex: number): number => {\r\n if (focusedGroup !== groupIndex) return 0;\r\n const group = groups[groupIndex];\r\n if (!group) return 0;\r\n const textBeforeCursor = group.value.slice(0, cursorPosRef.current);\r\n return measureTextWidth(textBeforeCursor);\r\n },\r\n [focusedGroup, groups, measureTextWidth]\r\n );\r\n\r\n // \uAC15\uC81C \uB9AC\uB80C\uB354\uB9C1\r\n const forceRender = useCallback(() => {\r\n setRenderTrigger((prev) => prev + 1);\r\n }, []);\r\n\r\n // MouseDown \uD578\uB4E4\uB7EC \uC0DD\uC131\r\n const createMouseDownHandler = useCallback(\r\n (groupIndex: number) => {\r\n return (e: React.MouseEvent) => {\r\n if (disabled || readonly) return;\r\n isClickFocusRef.current = true;\r\n const group = groups[groupIndex];\r\n const newPos = getCursorPosFromClick(e, group.value, group.ref);\r\n cursorPosRef.current = newPos;\r\n };\r\n },\r\n [disabled, readonly, groups, getCursorPosFromClick]\r\n );\r\n\r\n // Click \uD578\uB4E4\uB7EC \uC0DD\uC131 - \uD074\uB9AD \uC2DC \uC804\uCCB4 \uC120\uD0DD\r\n const createClickHandler = useCallback(\r\n (groupIndex: number) => {\r\n return (e: React.MouseEvent) => {\r\n if (disabled || readonly) return;\r\n e.stopPropagation();\r\n\r\n const group = groups[groupIndex];\r\n const input = group.ref.current;\r\n\r\n // \uAC12\uC774 \uC788\uC73C\uBA74 \uC804\uCCB4 \uC120\uD0DD\r\n if (input && group.value.length > 0) {\r\n setTimeout(() => {\r\n input.setSelectionRange(0, group.value.length);\r\n }, 0);\r\n setCursorVisible(false); // \uC804\uCCB4 \uC120\uD0DD \uC2DC \uCEE4\uC11C \uC228\uAE40\r\n } else {\r\n // \uAC12\uC774 \uC5C6\uC73C\uBA74 \uCEE4\uC11C \uD45C\uC2DC\r\n cursorPosRef.current = 0;\r\n setCursorVisible(true);\r\n }\r\n\r\n setInputComplete(false);\r\n setFocusedGroup(groupIndex);\r\n setRenderTrigger((prev) => prev + 1);\r\n };\r\n },\r\n [disabled, readonly, groups]\r\n );\r\n\r\n // \uCEE8\uD14C\uC774\uB108 \uD074\uB9AD \uD578\uB4E4\uB7EC \uC0DD\uC131 - \uC678\uACFD \uD074\uB9AD \uC2DC \uCCAB \uBC88\uC9F8 \uBE48 \uCE78 \uB610\uB294 \uB9C8\uC9C0\uB9C9 \uCE78\uC73C\uB85C \uC774\uB3D9\r\n const createContainerClickHandler = useCallback(() => {\r\n return (e: React.MouseEvent) => {\r\n if (disabled || readonly) return;\r\n\r\n // \uC774\uBBF8 input\uC5D0\uC11C \uCC98\uB9AC\uB41C \uD074\uB9AD\uC774\uBA74 \uBB34\uC2DC\r\n const target = e.target as HTMLElement;\r\n if (target.tagName === \"INPUT\") return;\r\n\r\n // \uCCAB \uBC88\uC9F8 \uBE48 \uCE78 \uCC3E\uAE30\r\n let targetIndex = -1;\r\n for (let i = 0; i < groups.length; i++) {\r\n if (groups[i].value.length === 0) {\r\n targetIndex = i;\r\n break;\r\n }\r\n }\r\n\r\n // \uBE48 \uCE78\uC774 \uC5C6\uC73C\uBA74 \uB9C8\uC9C0\uB9C9 \uCE78 \uC120\uD0DD\r\n if (targetIndex === -1) {\r\n targetIndex = groups.length - 1;\r\n }\r\n\r\n const targetGroup = groups[targetIndex];\r\n const input = targetGroup.ref.current;\r\n\r\n if (targetGroup.value.length > 0) {\r\n // \uAC12\uC774 \uC788\uC73C\uBA74 \uC804\uCCB4 \uC120\uD0DD\r\n input?.focus();\r\n setTimeout(() => {\r\n input?.setSelectionRange(0, targetGroup.value.length);\r\n }, 0);\r\n setCursorVisible(false);\r\n } else {\r\n // \uAC12\uC774 \uC5C6\uC73C\uBA74 \uCEE4\uC11C \uD45C\uC2DC\r\n cursorPosRef.current = 0;\r\n setCursorVisible(true);\r\n input?.focus();\r\n }\r\n\r\n setFocusedGroup(targetIndex);\r\n setInputComplete(false);\r\n setRenderTrigger((prev) => prev + 1);\r\n };\r\n }, [disabled, readonly, groups]);\r\n\r\n // Focus \uD578\uB4E4\uB7EC \uC0DD\uC131\r\n const createFocusHandler = useCallback(\r\n (groupIndex: number) => {\r\n return () => {\r\n const group = groups[groupIndex];\r\n\r\n // \uD074\uB9AD\uC73C\uB85C \uC778\uD55C \uD3EC\uCEE4\uC2A4\uBA74 \uD074\uB9AD \uD578\uB4E4\uB7EC\uC5D0\uC11C \uCC98\uB9AC \uC644\uB8CC\uB428\r\n if (isClickFocusRef.current) {\r\n isClickFocusRef.current = false;\r\n // \uD074\uB9AD \uD578\uB4E4\uB7EC\uC5D0\uC11C \uC804\uCCB4 \uC120\uD0DD \uCC98\uB9AC\uD588\uC73C\uBBC0\uB85C \uC5EC\uAE30\uC11C\uB294 \uC0C1\uD0DC\uB9CC \uC124\uC815\r\n setFocusedGroup(groupIndex);\r\n return;\r\n }\r\n\r\n // \uBC29\uD5A5\uD0A4\uB85C \uC778\uD55C \uD3EC\uCEE4\uC2A4\uBA74 \uC9C0\uC815\uB41C \uC704\uCE58\uB85C\r\n if (isArrowFocusRef.current) {\r\n isArrowFocusRef.current = false;\r\n cursorPosRef.current = arrowTargetPosRef.current;\r\n setCursorVisible(true);\r\n setFocusedGroup(groupIndex);\r\n setRenderTrigger((prev) => prev + 1);\r\n return;\r\n }\r\n\r\n // \uC77C\uBC18 \uD3EC\uCEE4\uC2A4 (\uD0ED \uB4F1) - \uC804\uCCB4 \uC120\uD0DD\r\n const input = group.ref.current;\r\n if (input && group.value.length > 0) {\r\n setTimeout(() => {\r\n input.setSelectionRange(0, group.value.length);\r\n }, 0);\r\n setCursorVisible(false);\r\n } else {\r\n cursorPosRef.current = 0;\r\n setCursorVisible(true);\r\n }\r\n setFocusedGroup(groupIndex);\r\n };\r\n },\r\n [groups]\r\n );\r\n\r\n // Blur \uD578\uB4E4\uB7EC \uC0DD\uC131\r\n const createBlurHandler = useCallback(() => {\r\n return () => {\r\n setTimeout(() => {\r\n const activeElement = document.activeElement;\r\n const isStillFocused = groups.some(\r\n (group) => group.ref.current === activeElement\r\n );\r\n if (!isStillFocused) {\r\n setCursorVisible(false);\r\n setFocusedGroup(null);\r\n }\r\n }, 0);\r\n };\r\n }, [groups]);\r\n\r\n // KeyDown \uD578\uB4E4\uB7EC \uC0DD\uC131\r\n const createKeyDownHandler = useCallback(\r\n (\r\n groupIndex: number,\r\n onValueChange?: (newValue: string, groupIndex: number) => void\r\n ) => {\r\n return (e: React.KeyboardEvent<HTMLInputElement>) => {\r\n const group = groups[groupIndex];\r\n const input = group.ref.current;\r\n const selectionStart = input?.selectionStart ?? 0;\r\n const selectionEnd = input?.selectionEnd ?? 0;\r\n const hasSelection = selectionEnd > selectionStart;\r\n\r\n if (e.key === \"ArrowLeft\") {\r\n // 3-2. \uD604\uC7AC\uCE78 \uCCAB\uBC88\uC9F8\uC5D0\uC11C \uC67C\uCABD\uD0A4 \u2192 \uC774\uC804\uCE78 \uB9C8\uC9C0\uB9C9 \uBB38\uC790\uB85C\r\n if (selectionStart === 0 && groupIndex > 0) {\r\n e.preventDefault();\r\n const prevGroup = groups[groupIndex - 1];\r\n // \uB9C8\uC9C0\uB9C9 \uBB38\uC790 \uC704\uCE58 (length - 1), \uBE48 \uACBD\uC6B0 0\r\n const newPos =\r\n prevGroup.value.length > 0\r\n ? prevGroup.value.length - 1\r\n : 0;\r\n isArrowFocusRef.current = true;\r\n arrowTargetPosRef.current = newPos;\r\n prevGroup.ref.current?.focus();\r\n prevGroup.ref.current?.setSelectionRange(\r\n newPos,\r\n newPos\r\n );\r\n } else if (selectionStart > 0) {\r\n e.preventDefault();\r\n const newPos = selectionStart - 1;\r\n input?.setSelectionRange(newPos, newPos);\r\n cursorPosRef.current = newPos;\r\n setCursorVisible(true);\r\n setRenderTrigger((prev) => prev + 1);\r\n }\r\n } else if (e.key === \"ArrowRight\") {\r\n // 3-1. \uD604\uC7AC\uCE78 \uB9C8\uC9C0\uB9C9 \uBB38\uC790\uC5D0\uC11C \uC624\uB978\uCABD\uD0A4 \u2192 \uB2E4\uC74C\uCE78 \uCCAB\uBC88\uC9F8\uB85C\r\n if (\r\n selectionEnd >= group.value.length - 1 &&\r\n group.value.length > 0 &&\r\n groupIndex < groups.length - 1\r\n ) {\r\n e.preventDefault();\r\n const nextGroup = groups[groupIndex + 1];\r\n isArrowFocusRef.current = true;\r\n arrowTargetPosRef.current = 0;\r\n nextGroup.ref.current?.focus();\r\n nextGroup.ref.current?.setSelectionRange(0, 0);\r\n } else if (selectionEnd < group.value.length - 1) {\r\n // \uB9C8\uC9C0\uB9C9 \uBB38\uC790 \uC804\uAE4C\uC9C0\uB9CC \uC624\uB978\uCABD \uC774\uB3D9\r\n e.preventDefault();\r\n const newPos = selectionEnd + 1;\r\n input?.setSelectionRange(newPos, newPos);\r\n cursorPosRef.current = newPos;\r\n setCursorVisible(true);\r\n setRenderTrigger((prev) => prev + 1);\r\n }\r\n } else if (e.key === \"Backspace\") {\r\n // \uC804\uCCB4 \uC120\uD0DD \uC0C1\uD0DC\uC5D0\uC11C\uB294 \uC804\uCCB4 \uC0AD\uC81C\r\n if (hasSelection) {\r\n e.preventDefault();\r\n const newValue =\r\n group.value.slice(0, selectionStart) +\r\n group.value.slice(selectionEnd);\r\n group.setValue(newValue);\r\n cursorPosRef.current = selectionStart;\r\n setCursorVisible(true);\r\n setInputComplete(false);\r\n setRenderTrigger((prev) => prev + 1);\r\n setTimeout(() => {\r\n input?.setSelectionRange(\r\n selectionStart,\r\n selectionStart\r\n );\r\n }, 0);\r\n onValueChange?.(newValue, groupIndex);\r\n } else if (selectionStart > 0) {\r\n // 1-3. \uD604\uC7AC\uCEE4\uC11C \uC55E\uC790\uB9AC \uC0AD\uC81C\r\n e.preventDefault();\r\n const newValue =\r\n group.value.slice(0, selectionStart - 1) +\r\n group.value.slice(selectionStart);\r\n group.setValue(newValue);\r\n const newPos = selectionStart - 1;\r\n cursorPosRef.current = newPos;\r\n setCursorVisible(true);\r\n setInputComplete(false);\r\n setRenderTrigger((prev) => prev + 1);\r\n setTimeout(() => {\r\n input?.setSelectionRange(newPos, newPos);\r\n }, 0);\r\n onValueChange?.(newValue, groupIndex);\r\n } else if (groupIndex > 0) {\r\n // 1-4. \uD604\uC7AC\uCE78 \uC81C\uC77C \uC55E\uC5D0\uC11C \u2192 \uC774\uC804\uCE78 \uB9C8\uC9C0\uB9C9 \uC790\uB9AC\uAC12 \uC0AD\uC81C \uB610\uB294 \uCEE4\uC11C \uC774\uB3D9\r\n e.preventDefault();\r\n const prevGroup = groups[groupIndex - 1];\r\n const prevLen = prevGroup.value.length;\r\n\r\n if (prevLen > 0) {\r\n // \uC774\uC804 \uCE78\uC5D0 \uAC12\uC774 \uC788\uC73C\uBA74 \uB9C8\uC9C0\uB9C9 \uAC12 \uC0AD\uC81C\r\n const newPrevValue = prevGroup.value.slice(0, -1);\r\n prevGroup.setValue(newPrevValue);\r\n cursorPosRef.current = newPrevValue.length;\r\n setInputComplete(false);\r\n onValueChange?.(newPrevValue, groupIndex - 1);\r\n } else {\r\n // \uC774\uC804 \uCE78\uC774 \uBE44\uC5B4\uC788\uC73C\uBA74 \uCEE4\uC11C\uB9CC \uC774\uB3D9\r\n cursorPosRef.current = 0;\r\n }\r\n\r\n setCursorVisible(true);\r\n setFocusedGroup(groupIndex - 1);\r\n setRenderTrigger((prev) => prev + 1);\r\n prevGroup.ref.current?.focus();\r\n const finalPos = prevLen > 0 ? prevLen - 1 : 0;\r\n prevGroup.ref.current?.setSelectionRange(\r\n finalPos,\r\n finalPos\r\n );\r\n }\r\n } else if (e.key === \"Delete\") {\r\n // \uC804\uCCB4 \uC120\uD0DD \uC0C1\uD0DC\uC5D0\uC11C\uB294 \uC804\uCCB4 \uC0AD\uC81C\r\n if (hasSelection) {\r\n e.preventDefault();\r\n const newValue =\r\n group.value.slice(0, selectionStart) +\r\n group.value.slice(selectionEnd);\r\n group.setValue(newValue);\r\n cursorPosRef.current = selectionStart;\r\n setCursorVisible(true);\r\n setInputComplete(false);\r\n setRenderTrigger((prev) => prev + 1);\r\n setTimeout(() => {\r\n input?.setSelectionRange(\r\n selectionStart,\r\n selectionStart\r\n );\r\n }, 0);\r\n onValueChange?.(newValue, groupIndex);\r\n } else if (selectionStart < group.value.length) {\r\n // 1-5. del \uD0A4\uB85C \uD604\uC7AC \uCEE4\uC11C \uC704\uCE58 \uAC12 \uC0AD\uC81C\r\n e.preventDefault();\r\n const newValue =\r\n group.value.slice(0, selectionStart) +\r\n group.value.slice(selectionStart + 1);\r\n group.setValue(newValue);\r\n cursorPosRef.current = selectionStart;\r\n setCursorVisible(true);\r\n setInputComplete(false);\r\n setRenderTrigger((prev) => prev + 1);\r\n setTimeout(() => {\r\n input?.setSelectionRange(\r\n selectionStart,\r\n selectionStart\r\n );\r\n }, 0);\r\n onValueChange?.(newValue, groupIndex);\r\n }\r\n }\r\n };\r\n },\r\n [groups]\r\n );\r\n\r\n // Change \uD578\uB4E4\uB7EC \uC0DD\uC131\r\n const createChangeHandler = useCallback(\r\n (\r\n groupIndex: number,\r\n onAfterChange?: (\r\n newValue: string,\r\n groupIndex: number,\r\n isComplete: boolean\r\n ) => void\r\n ) => {\r\n return (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const group = groups[groupIndex];\r\n const numbers = e.target.value\r\n .replace(/\\D/g, \"\")\r\n .slice(0, group.maxLength);\r\n group.setValue(numbers);\r\n cursorPosRef.current = numbers.length;\r\n setCursorVisible(true); // \uC785\uB825 \uC2DC \uCEE4\uC11C \uD45C\uC2DC\r\n\r\n // \uCD5C\uB300 \uAE38\uC774 \uC785\uB825 \uC2DC \uB2E4\uC74C \uADF8\uB8F9\uC73C\uB85C \uC774\uB3D9\r\n const isGroupComplete = numbers.length === group.maxLength;\r\n if (isGroupComplete && groupIndex < groups.length - 1) {\r\n setTimeout(() => {\r\n const nextGroup = groups[groupIndex + 1];\r\n nextGroup.ref.current?.focus();\r\n nextGroup.ref.current?.setSelectionRange(0, 0);\r\n cursorPosRef.current = 0;\r\n }, 0);\r\n }\r\n\r\n // \uC804\uCCB4 \uC644\uB8CC \uCCB4\uD06C\r\n const allComplete =\r\n isGroupComplete &&\r\n groupIndex === groups.length - 1 &&\r\n groups\r\n .slice(0, -1)\r\n .every((g) => g.value.length === g.maxLength);\r\n\r\n if (allComplete) {\r\n setInputComplete(true);\r\n onComplete?.();\r\n } else {\r\n setInputComplete(false);\r\n }\r\n\r\n setRenderTrigger((prev) => prev + 1);\r\n onAfterChange?.(numbers, groupIndex, allComplete);\r\n };\r\n },\r\n [groups, onComplete]\r\n );\r\n\r\n return {\r\n focusedGroup,\r\n setFocusedGroup,\r\n cursorVisible,\r\n setCursorVisible,\r\n cursorPosRef,\r\n isClickFocusRef,\r\n inputComplete,\r\n setInputComplete,\r\n renderTrigger,\r\n measureTextWidth,\r\n getCursorLeft,\r\n createMouseDownHandler,\r\n createClickHandler,\r\n createContainerClickHandler,\r\n createFocusHandler,\r\n createBlurHandler,\r\n createKeyDownHandler,\r\n createChangeHandler,\r\n getCursorPosFromClick,\r\n forceRender,\r\n };\r\n}\r\n"],
5
5
  "mappings": "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",
6
6
  "names": ["address_exports", "__export", "AddressTextField", "__toCommonJS", "import_material", "import_Search", "import_Close", "import_react", "import_react_daum_postcode", "import_react", "useDebouncedEmit", "name", "debounce", "onChange", "lastEmittedValue", "debounceTimer", "emitChange", "e", "newValue", "immediate", "doEmit", "syntheticEvent", "flushOnBlur", "currentValue", "import_react", "import_react", "import_jsx_runtime", "AddressTextFieldBase", "value", "onChange", "readonly", "debounce", "onBlur", "size", "spellCheck", "externalInputRef", "props", "ref", "internalInputRef", "address", "setAddress", "isPostcodeOpen", "setIsPostcodeOpen", "iconSize", "emitChange", "flushOnBlur", "lastEmittedValue", "useDebouncedEmit", "strValue", "handlePostcodeComplete", "data", "fullAddress", "extraAddress", "handleSearchButtonClick", "handleKeyDown", "event", "handlePostcodeClose", "onClose", "e", "newValue", "SearchIcon", "CloseIcon", "DaumPostcode", "AddressTextFieldWithForm", "form", "name", "rest", "formValue", "handleFormChange", "AddressTextField"]
7
7
  }