@ehfuse/mui-form-controls 2.0.6 → 3.0.1

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.
package/README.md CHANGED
@@ -31,17 +31,17 @@ MUI 기반 폼 컨트롤과 텍스트 필드 컴포넌트 모음
31
31
 
32
32
  키보드 입력이 필요한 컴포넌트는 위 "컴포넌트"에, 마우스 조작만으로 선택/토글하는 컴포넌트는 여기서 분리해 정리했습니다.
33
33
 
34
- | 컴포넌트 | 설명 | 문서 |
35
- | ---------------- | --------------------------------------------------------- | ------------------------------------ |
36
- | **ButtonGroup** | 버튼 그룹 컴포넌트 | [API](./docs/ko/api.md#buttongroup) |
37
- | **Checkbox** | MUI Checkbox 래퍼 (폼 통합) | [API](./docs/ko/api.md#checkbox) |
38
- | **LabelSelect** | MUI Select 기반 선택 필드 (권장: 라벨 포함, form 바인딩) | [API](./docs/ko/api.md#labelselect) |
39
- | **RadioGroup** | MUI RadioGroup 래퍼 (폼 통합) | [API](./docs/ko/api.md#radiogroup) |
40
- | **Rating** | 평점 입력 컴포넌트 | [API](./docs/ko/api.md#rating) |
41
- | **Slider** | MUI Slider 래퍼 (폼 통합) | [API](./docs/ko/api.md#slider) |
42
- | **Stepper** | 스텝 진행 표시 컴포넌트 | [API](./docs/ko/api.md#stepper) |
43
- | **Switch** | MUI Switch 래퍼 (폼 통합) | [API](./docs/ko/api.md#switch) |
44
- | **ToggleButton** | 토글 버튼 컴포넌트 | [API](./docs/ko/api.md#togglebutton) |
34
+ | 컴포넌트 | 설명 | 문서 |
35
+ | --------------------- | ------------------------------------------------------------ | ----------------------------------------- |
36
+ | **ButtonGroup** | 버튼 그룹 컴포넌트 | [API](./docs/ko/api.md#buttongroup) |
37
+ | **Checkbox** | MUI Checkbox 래퍼 (폼 통합) | [API](./docs/ko/api.md#checkbox) |
38
+ | **LabelSelect** | MUI Select 기반 선택 필드 (권장: 라벨 포함, form 바인딩) | [API](./docs/ko/api.md#labelselect) |
39
+ | **RadioGroup** | MUI RadioGroup 래퍼 (폼 통합) | [API](./docs/ko/api.md#radiogroup) |
40
+ | **Rating** | 평점 입력 컴포넌트 | [API](./docs/ko/api.md#rating) |
41
+ | **Slider** | MUI Slider 래퍼 (폼 통합) | [API](./docs/ko/api.md#slider) |
42
+ | **Stepper** | 스텝 진행 표시 컴포넌트 | [API](./docs/ko/api.md#stepper) |
43
+ | **Switch** | MUI Switch 래퍼 (폼 통합) | [API](./docs/ko/api.md#switch) |
44
+ | **ToggleButton** | 토글 버튼 컴포넌트 | [API](./docs/ko/api.md#togglebutton) |
45
45
  | **ToggleButtonGroup** | 옵션 기반 토글 그룹 필드 (`ToggleButtonGroup` + form 바인딩) | [API](./docs/ko/api.md#togglebuttongroup) |
46
46
 
47
47
  ## 훅 (Hooks)
@@ -57,6 +57,12 @@ MUI 기반 폼 컨트롤과 텍스트 필드 컴포넌트 모음
57
57
  npm install @ehfuse/mui-form-controls
58
58
  ```
59
59
 
60
+ `AddressTextField`를 사용할 때만 다음 우편번호 의존성을 추가로 설치합니다.
61
+
62
+ ```bash
63
+ npm install react-daum-postcode
64
+ ```
65
+
60
66
  ## 필수 의존성
61
67
 
62
68
  ```json
@@ -81,7 +87,6 @@ import {
81
87
  PhoneTextField,
82
88
  EmailTextField,
83
89
  NumberTextField,
84
- AddressTextField,
85
90
  JuminTextField,
86
91
  BizNumTextField,
87
92
  CardNumTextField,
@@ -110,6 +115,8 @@ import {
110
115
  useKoreanHolidaysRange,
111
116
  } from '@ehfuse/mui-form-controls';
112
117
 
118
+ import { AddressTextField } from '@ehfuse/mui-form-controls/address';
119
+
113
120
  // 검색 필드
114
121
  <SearchTextField
115
122
  value={search}
@@ -282,17 +289,17 @@ import { Switch } from "@ehfuse/mui-form-controls";
282
289
  import { ToggleButtonGroup } from "@ehfuse/mui-form-controls";
283
290
 
284
291
  <ToggleButtonGroup
285
- form={form}
286
- name="contractor_gender"
287
- exclusive
288
- options={[
289
- { label: "남성", value: "M" },
290
- { label: "여성", value: "F" },
291
- ]}
292
- onDeselect="clear"
293
- fullWidth
294
- size="small"
295
- />
292
+ form={form}
293
+ name="contractor_gender"
294
+ exclusive
295
+ options={[
296
+ { label: "남성", value: "M" },
297
+ { label: "여성", value: "F" },
298
+ ]}
299
+ onDeselect="clear"
300
+ fullWidth
301
+ size="small"
302
+ />;
296
303
  ```
297
304
 
298
305
  ## 문서 / Documentation
@@ -0,0 +1,2 @@
1
+ export { AddressTextField } from "./AddressTextField";
2
+ export type { AddressTextFieldProps, DaumPostcodeData } from "./types";
@@ -0,0 +1,49 @@
1
+ "use strict";var Y=Object.create;var I=Object.defineProperty;var N=Object.getOwnPropertyDescriptor;var K=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,J=Object.prototype.hasOwnProperty;var _=(n,e)=>{for(var t in e)I(n,t,{get:e[t],enumerable:!0})},L=(n,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of K(e))!J.call(n,o)&&o!==t&&I(n,o,{get:()=>e[o],enumerable:!(s=N(e,o))||s.enumerable});return n};var H=(n,e,t)=>(t=n!=null?Y(W(n)):{},L(e||!n||!n.__esModule?I(t,"default",{value:n,enumerable:!0}):t,n)),X=n=>L(I({},"__esModule",{value:!0}),n);var Q={};_(Q,{AddressTextField:()=>V});module.exports=X(Q);var a=require("@mui/material"),S=H(require("@mui/icons-material/Search")),A=H(require("@mui/icons-material/Close")),u=require("react"),P=H(require("react-daum-postcode"));var f=require("react");function M({name:n="",debounce:e,onChange:t}){let s=(0,f.useRef)(""),o=(0,f.useRef)(null);(0,f.useEffect)(()=>()=>{o.current&&clearTimeout(o.current)},[]);let g=(0,f.useCallback)((c,i,m=!1)=>{if(i===s.current)return;let E=()=>{if(i!==s.current&&(s.current=i,t)){let b={...c||{},target:{...c?.target||{},name:n,value:i}};t(b)}};o.current&&clearTimeout(o.current),m||e===void 0||e===0?E():o.current=setTimeout(E,e)},[t,e,n]),d=(0,f.useCallback)((c,i)=>{if(o.current&&(clearTimeout(o.current),o.current=null,i!==s.current&&(s.current=i,t))){let m={...c,target:{...c.target,name:n,value:i}};t(m)}},[t,n]);return{emitChange:g,flushOnBlur:d,lastEmittedValue:s,debounceTimer:o}}var D=require("react");var w=require("react");var l=require("react/jsx-runtime");var O=(0,u.forwardRef)(function({value:e,onChange:t,readonly:s,debounce:o,onBlur:g,size:d,spellCheck:c=!1,inputRef:i,...m},E){let b=(0,u.useRef)(null);(0,u.useImperativeHandle)(i,()=>b.current,[]);let[T,R]=(0,u.useState)(typeof e=="string"?e:""),[k,y]=(0,u.useState)(!1),B=d==="small"?20:24,{emitChange:C,flushOnBlur:G,lastEmittedValue:v}=M({name:m.name,debounce:o,onChange:t});(0,u.useEffect)(()=>{if(e!=null){let r=String(e);r!==v.current&&(R(r),v.current=r)}else v.current!==""&&(R(""),v.current="")},[e,v]);let U=r=>{let h=r.address,p="";r.addressType==="R"&&(r.bname!==""&&/[동|로|가]$/g.test(r.bname)&&(p+=r.bname),r.buildingName!==""&&r.apartment==="Y"&&(p+=p!==""?", "+r.buildingName:r.buildingName),p!==""&&(p=" ("+p+")"),h+=p),R(h),C(null,h,!0),y(!1)},x=()=>{y(!0)},$=r=>{r.key==="Enter"&&(r.preventDefault(),x())},F=()=>{y(!1)},j=()=>{y(!1),C(null,T,!0)};return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(a.TextField,{...m,ref:E,inputRef:b,size:d,value:T,onChange:s?void 0:r=>{let h=r.target.value;R(h),C(r,h)},autoComplete:"off",spellCheck:c,onKeyDown:s?void 0:$,onBlur:s?void 0:r=>{G(r,T),g&&g(r)},sx:{...s?{"& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline":{borderColor:"rgba(0, 0, 0, 0.23)",borderWidth:1},"& .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline":{borderColor:"rgba(0, 0, 0, 0.23)"},"& .MuiInputLabel-root.Mui-focused":{color:"rgba(0, 0, 0, 0.6)"}}:{},...s&&{pointerEvents:"none"}},slotProps:{...m.slotProps,input:{...m.slotProps?.input,readOnly:s,endAdornment:s?void 0:(0,l.jsx)(a.InputAdornment,{position:"end",sx:{mr:d==="small"?-.5:.5},children:(0,l.jsx)(a.IconButton,{onClick:x,edge:"end","aria-label":"\uC8FC\uC18C \uCC3E\uAE30",size:d,children:(0,l.jsx)(S.default,{sx:{fontSize:B}})})})}}}),(0,l.jsxs)(a.Dialog,{open:k,onClose:F,maxWidth:"sm",fullWidth:!0,slotProps:{paper:{sx:{height:"550px"}}},children:[(0,l.jsxs)(a.DialogTitle,{sx:{display:"flex",justifyContent:"space-between",alignItems:"center",pr:2},children:["\uC8FC\uC18C \uAC80\uC0C9",(0,l.jsx)(a.IconButton,{onClick:j,size:"small",children:(0,l.jsx)(A.default,{})})]}),(0,l.jsx)(a.DialogContent,{style:{padding:0},dividers:!0,children:(0,l.jsx)(P.default,{onComplete:U,onClose:F,defaultQuery:T,style:{width:"100%",height:"100%"}})})]})]})}),q=(0,u.forwardRef)(function({form:e,name:t,onChange:s,...o},g){if(!e||typeof e.useFormValue!="function")throw new Error("AddressTextField form prop is missing. Provide a form from useForm or useGlobalForm.");if(!t)throw new Error("AddressTextField requires a name when form prop is provided.");let d=e.useFormValue(t),c=(0,u.useCallback)(i=>{e.handleFormChange(i),s?.(i)},[e,s]);return(0,l.jsx)(O,{...o,ref:g,name:t,value:d,onChange:c})}),V=(0,u.forwardRef)(function(e,t){return e.form?(0,l.jsx)(q,{...e,ref:t}):(0,l.jsx)(O,{...e,ref:t})});0&&(module.exports={AddressTextField});
2
+ /**
3
+ * useTextFieldBase.ts
4
+ *
5
+ * TextField 컴포넌트들의 공통 로직을 담당하는 훅
6
+ * - 디바운스 처리
7
+ * - 내부 상태 관리
8
+ * - blur 시 flush
9
+ * - 외부 value 동기화
10
+ *
11
+ * @license MIT
12
+ * @copyright 2025 김영진 (Kim Young Jin)
13
+ * @author 김영진 (ehfuse@gmail.com)
14
+ */
15
+ /**
16
+ * useKoreanHolidays.ts
17
+ *
18
+ * 한국천문연구원 특일 정보 API를 이용하여 공휴일을 조회하는 커스텀 훅
19
+ *
20
+ * @license MIT
21
+ * @copyright 2025 김영진 (Kim Young Jin)
22
+ * @author 김영진 (ehfuse@gmail.com)
23
+ */
24
+ /**
25
+ * useGroupedInput.ts
26
+ *
27
+ * 그룹화된 입력 필드(주민번호, 사업자번호, 시간 등)에서 공통으로 사용하는
28
+ * 커서 관리, 포커스 관리, 키보드 핸들링 로직을 제공하는 훅
29
+ *
30
+ * @license MIT
31
+ * @copyright 2025 김영진 (Kim Young Jin)
32
+ * @author 김영진 (ehfuse@gmail.com)
33
+ */
34
+ /**
35
+ * hooks/index.ts
36
+ *
37
+ * @license MIT
38
+ * @copyright 2025 김영진 (Kim Young Jin)
39
+ * @author 김영진 (ehfuse@gmail.com)
40
+ */
41
+ /**
42
+ * AddressTextField.tsx
43
+ *
44
+ * @license MIT
45
+ form,
46
+ * @copyright 2025 김영진 (Kim Young Jin)
47
+ * @author 김영진 (ehfuse@gmail.com)
48
+ */
49
+ //# sourceMappingURL=address.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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\";\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
+ "mappings": "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",
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
+ }
@@ -0,0 +1,49 @@
1
+ import{IconButton as w,InputAdornment as B,TextField as G,Dialog as U,DialogContent as $,DialogTitle as j}from"@mui/material";import Y from"@mui/icons-material/Search";import N from"@mui/icons-material/Close";import{useState as x,useEffect as K,useCallback as W,forwardRef as I,useRef as J,useImperativeHandle as _}from"react";import X from"react-daum-postcode";import{useState as ne,useRef as M,useEffect as k,useCallback as D}from"react";function R({name:c="",debounce:t,onChange:r}){let n=M(""),s=M(null);k(()=>()=>{s.current&&clearTimeout(s.current)},[]);let d=D((u,o,a=!1)=>{if(o===n.current)return;let g=()=>{if(o!==n.current&&(n.current=o,r)){let h={...u||{},target:{...u?.target||{},name:c,value:o}};r(h)}};s.current&&clearTimeout(s.current),a||t===void 0||t===0?g():s.current=setTimeout(g,t)},[r,t,c]),i=D((u,o)=>{if(s.current&&(clearTimeout(s.current),s.current=null,o!==n.current&&(n.current=o,r))){let a={...u,target:{...u.target,name:c,value:o}};r(a)}},[r,c]);return{emitChange:d,flushOnBlur:i,lastEmittedValue:n,debounceTimer:s}}import{useState as le,useEffect as ue,useCallback as ie}from"react";import{useRef as fe,useState as de,useCallback as me}from"react";import{Fragment as z,jsx as l,jsxs as y}from"react/jsx-runtime";var F=I(function({value:t,onChange:r,readonly:n,debounce:s,onBlur:d,size:i,spellCheck:u=!1,inputRef:o,...a},g){let h=J(null);_(o,()=>h.current,[]);let[v,E]=x(typeof t=="string"?t:""),[L,b]=x(!1),S=i==="small"?20:24,{emitChange:T,flushOnBlur:A,lastEmittedValue:p}=R({name:a.name,debounce:s,onChange:r});K(()=>{if(t!=null){let e=String(t);e!==p.current&&(E(e),p.current=e)}else p.current!==""&&(E(""),p.current="")},[t,p]);let P=e=>{let m=e.address,f="";e.addressType==="R"&&(e.bname!==""&&/[동|로|가]$/g.test(e.bname)&&(f+=e.bname),e.buildingName!==""&&e.apartment==="Y"&&(f+=f!==""?", "+e.buildingName:e.buildingName),f!==""&&(f=" ("+f+")"),m+=f),E(m),T(null,m,!0),b(!1)},C=()=>{b(!0)},O=e=>{e.key==="Enter"&&(e.preventDefault(),C())},H=()=>{b(!1)},V=()=>{b(!1),T(null,v,!0)};return y(z,{children:[l(G,{...a,ref:g,inputRef:h,size:i,value:v,onChange:n?void 0:e=>{let m=e.target.value;E(m),T(e,m)},autoComplete:"off",spellCheck:u,onKeyDown:n?void 0:O,onBlur:n?void 0:e=>{A(e,v),d&&d(e)},sx:{...n?{"& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline":{borderColor:"rgba(0, 0, 0, 0.23)",borderWidth:1},"& .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline":{borderColor:"rgba(0, 0, 0, 0.23)"},"& .MuiInputLabel-root.Mui-focused":{color:"rgba(0, 0, 0, 0.6)"}}:{},...n&&{pointerEvents:"none"}},slotProps:{...a.slotProps,input:{...a.slotProps?.input,readOnly:n,endAdornment:n?void 0:l(B,{position:"end",sx:{mr:i==="small"?-.5:.5},children:l(w,{onClick:C,edge:"end","aria-label":"\uC8FC\uC18C \uCC3E\uAE30",size:i,children:l(Y,{sx:{fontSize:S}})})})}}}),y(U,{open:L,onClose:H,maxWidth:"sm",fullWidth:!0,slotProps:{paper:{sx:{height:"550px"}}},children:[y(j,{sx:{display:"flex",justifyContent:"space-between",alignItems:"center",pr:2},children:["\uC8FC\uC18C \uAC80\uC0C9",l(w,{onClick:V,size:"small",children:l(N,{})})]}),l($,{style:{padding:0},dividers:!0,children:l(X,{onComplete:P,onClose:H,defaultQuery:v,style:{width:"100%",height:"100%"}})})]})]})}),q=I(function({form:t,name:r,onChange:n,...s},d){if(!t||typeof t.useFormValue!="function")throw new Error("AddressTextField form prop is missing. Provide a form from useForm or useGlobalForm.");if(!r)throw new Error("AddressTextField requires a name when form prop is provided.");let i=t.useFormValue(r),u=W(o=>{t.handleFormChange(o),n?.(o)},[t,n]);return l(F,{...s,ref:d,name:r,value:i,onChange:u})}),Q=I(function(t,r){return t.form?l(q,{...t,ref:r}):l(F,{...t,ref:r})});export{Q as AddressTextField};
2
+ /**
3
+ * useTextFieldBase.ts
4
+ *
5
+ * TextField 컴포넌트들의 공통 로직을 담당하는 훅
6
+ * - 디바운스 처리
7
+ * - 내부 상태 관리
8
+ * - blur 시 flush
9
+ * - 외부 value 동기화
10
+ *
11
+ * @license MIT
12
+ * @copyright 2025 김영진 (Kim Young Jin)
13
+ * @author 김영진 (ehfuse@gmail.com)
14
+ */
15
+ /**
16
+ * useKoreanHolidays.ts
17
+ *
18
+ * 한국천문연구원 특일 정보 API를 이용하여 공휴일을 조회하는 커스텀 훅
19
+ *
20
+ * @license MIT
21
+ * @copyright 2025 김영진 (Kim Young Jin)
22
+ * @author 김영진 (ehfuse@gmail.com)
23
+ */
24
+ /**
25
+ * useGroupedInput.ts
26
+ *
27
+ * 그룹화된 입력 필드(주민번호, 사업자번호, 시간 등)에서 공통으로 사용하는
28
+ * 커서 관리, 포커스 관리, 키보드 핸들링 로직을 제공하는 훅
29
+ *
30
+ * @license MIT
31
+ * @copyright 2025 김영진 (Kim Young Jin)
32
+ * @author 김영진 (ehfuse@gmail.com)
33
+ */
34
+ /**
35
+ * hooks/index.ts
36
+ *
37
+ * @license MIT
38
+ * @copyright 2025 김영진 (Kim Young Jin)
39
+ * @author 김영진 (ehfuse@gmail.com)
40
+ */
41
+ /**
42
+ * AddressTextField.tsx
43
+ *
44
+ * @license MIT
45
+ form,
46
+ * @copyright 2025 김영진 (Kim Young Jin)
47
+ * @author 김영진 (ehfuse@gmail.com)
48
+ */
49
+ //# sourceMappingURL=address.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/AddressTextField.tsx", "../src/hooks/useTextFieldBase.ts", "../src/hooks/useKoreanHolidays.ts", "../src/hooks/useGroupedInput.ts"],
4
+ "sourcesContent": ["/**\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
+ "mappings": "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",
6
+ "names": ["IconButton", "InputAdornment", "TextField", "Dialog", "DialogContent", "DialogTitle", "SearchIcon", "CloseIcon", "useState", "useEffect", "useCallback", "forwardRef", "useRef", "useImperativeHandle", "DaumPostcode", "useState", "useRef", "useEffect", "useCallback", "useDebouncedEmit", "name", "debounce", "onChange", "lastEmittedValue", "useRef", "debounceTimer", "useEffect", "emitChange", "useCallback", "e", "newValue", "immediate", "doEmit", "syntheticEvent", "flushOnBlur", "currentValue", "useState", "useEffect", "useCallback", "useRef", "useState", "useCallback", "Fragment", "jsx", "jsxs", "AddressTextFieldBase", "forwardRef", "value", "onChange", "readonly", "debounce", "onBlur", "size", "spellCheck", "externalInputRef", "props", "ref", "internalInputRef", "useRef", "useImperativeHandle", "address", "setAddress", "useState", "isPostcodeOpen", "setIsPostcodeOpen", "iconSize", "emitChange", "flushOnBlur", "lastEmittedValue", "useDebouncedEmit", "useEffect", "strValue", "handlePostcodeComplete", "data", "fullAddress", "extraAddress", "handleSearchButtonClick", "handleKeyDown", "event", "handlePostcodeClose", "onClose", "TextField", "newValue", "InputAdornment", "IconButton", "SearchIcon", "Dialog", "DialogTitle", "CloseIcon", "DialogContent", "DaumPostcode", "AddressTextFieldWithForm", "form", "name", "rest", "formValue", "handleFormChange", "useCallback", "AddressTextField"]
7
+ }
package/dist/index.d.ts CHANGED
@@ -24,8 +24,8 @@ export { NumberField, NumberSpinner } from "./NumberField";
24
24
  export type { NumberFieldProps } from "./NumberField";
25
25
  export { Autocomplete } from "./Autocomplete";
26
26
  export { LabelSelect } from "./LabelSelect";
27
- export { PhoneTextField } from "./PhoneTextField";
28
27
  export { AddressTextField } from "./AddressTextField";
28
+ export { PhoneTextField } from "./PhoneTextField";
29
29
  export { NumberTextField } from "./NumberTextField";
30
30
  export { JuminTextField } from "./JuminTextField";
31
31
  export { VerificationCodeTextField } from "./VerificationCodeTextField";
@@ -37,4 +37,4 @@ export { BizNumTextField } from "./BizNumTextField";
37
37
  export { CardNumTextField } from "./CardNumTextField";
38
38
  export { useKoreanHolidays, useKoreanHolidaysRange } from "./hooks";
39
39
  export { CardIcon, VisaIcon, MastercardIcon, AmexIcon, JcbIcon, DinersIcon, DiscoverIcon, UnionPayIcon, BcIcon, } from "./icons";
40
- export type { SearchTextFieldProps, ClearTextFieldProps, PasswordTextFieldProps, TextFieldProps, TextAreaProps, CheckboxProps, SwitchProps, RadioGroupProps, RadioOption, DateRangeProps, SliderProps, RatingProps, ToggleButtonProps, ToggleButtonGroupProps, ToggleButtonGroupOption, ButtonGroupProps, StepperProps, AutocompleteProps, AutocompleteOption, LabelSelectProps, LabelSelectOption, PasswordValidationRules, PasswordValidationResult, PhoneTextFieldProps, PhoneFormat, AddressTextFieldProps, DaumPostcodeData, NumberTextFieldProps, JuminTextFieldProps, JuminInfo, VerificationCodeTextFieldProps, VerificationCodeType, EmailTextFieldProps, CustomDomain, DateTextFieldProps, DateFormat, TimeTextFieldProps, TimeFormat, DateTimeTextFieldProps, DateTimeFormat, BizNumTextFieldProps, CardNumTextFieldProps, SelectChangeEvent, } from "./types";
40
+ export type { SearchTextFieldProps, ClearTextFieldProps, PasswordTextFieldProps, TextFieldProps, TextAreaProps, CheckboxProps, SwitchProps, RadioGroupProps, RadioOption, DateRangeProps, SliderProps, RatingProps, ToggleButtonProps, ToggleButtonGroupProps, ToggleButtonGroupOption, ButtonGroupProps, StepperProps, AutocompleteProps, AutocompleteOption, LabelSelectProps, LabelSelectOption, AddressTextFieldProps, DaumPostcodeData, PasswordValidationRules, PasswordValidationResult, PhoneTextFieldProps, PhoneFormat, NumberTextFieldProps, JuminTextFieldProps, JuminInfo, VerificationCodeTextFieldProps, VerificationCodeType, EmailTextFieldProps, CustomDomain, DateTextFieldProps, DateFormat, TimeTextFieldProps, TimeFormat, DateTimeTextFieldProps, DateTimeFormat, BizNumTextFieldProps, CardNumTextFieldProps, SelectChangeEvent, } from "./types";