From 6855192c9398452d09c2e63011e09119a8cfa9b2 Mon Sep 17 00:00:00 2001 From: chacha Date: Tue, 24 Sep 2024 23:22:04 +0900 Subject: [PATCH 1/7] feat: add NewPhoneInput --- .../common/components/Forms/NewPhoneInput.tsx | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 packages/web/src/common/components/Forms/NewPhoneInput.tsx diff --git a/packages/web/src/common/components/Forms/NewPhoneInput.tsx b/packages/web/src/common/components/Forms/NewPhoneInput.tsx new file mode 100644 index 0000000..ec53597 --- /dev/null +++ b/packages/web/src/common/components/Forms/NewPhoneInput.tsx @@ -0,0 +1,215 @@ +/* +--- 사용 방법 (예시) --- + +*/ + +import React, { + ChangeEvent, + ChangeEventHandler, + InputHTMLAttributes, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; + +import isPropValid from "@emotion/is-prop-valid"; +import styled, { css } from "styled-components"; + +import ErrorMessage from "./_atomic/ErrorMessage"; +import Label from "./_atomic/Label"; + +export interface PhoneInputProps + extends InputHTMLAttributes { + label?: string; + placeholder: string; + area?: boolean; + disabled?: boolean; + value?: string; + handleChange?: (value: string) => void; + setErrorStatus?: (hasError: boolean) => void; + onChange?: ChangeEventHandler; +} + +const errorBorderStyle = css` + border-color: ${({ theme }) => theme.colors.RED[600]}; +`; + +const disabledStyle = css` + background-color: ${({ theme }) => theme.colors.GRAY[100]}; + border-color: ${({ theme }) => theme.colors.GRAY[200]}; +`; + +const areaInputStyle = css` + height: 100px; + resize: none; + overflow: auto; +`; + +const Input = styled.input + .withConfig({ + shouldForwardProp: prop => + isPropValid(prop) && prop !== "hasError" && prop !== "area", + }) + .attrs(({ area }) => ({ + as: area ? "textarea" : "input", + }))` + display: block; + width: 100%; + padding: 8px 12px 8px 12px; + outline: none; + border: 1px solid ${({ theme }) => theme.colors.GRAY[200]}; + border-radius: 4px; + gap: 8px; + font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD}; + font-size: 16px; + line-height: 20px; + font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR}; + color: ${({ theme }) => theme.colors.BLACK}; + background-color: ${({ theme }) => theme.colors.WHITE}; + &:focus { + border-color: ${({ theme, hasError, disabled }) => + !hasError && !disabled && theme.colors.PRIMARY}; + } + &:hover:not(:focus) { + border-color: ${({ theme, hasError, disabled }) => + !hasError && !disabled && theme.colors.GRAY[300]}; + } + &::placeholder { + color: ${({ theme }) => theme.colors.GRAY[200]}; + } + ${({ disabled }) => disabled && disabledStyle} + ${({ hasError }) => hasError && errorBorderStyle} + ${({ area }) => area && areaInputStyle} // TextAreaInput +`; + +const InputWrapper = styled.div` + width: 100%; + flex-direction: column; + display: flex; + gap: 4px; +`; + +// Component +const PhoneInput: React.FC = ({ + label = "", + placeholder, + area = false, + disabled = false, + value = "", + handleChange = () => {}, // setValue + setErrorStatus = () => {}, + onChange = undefined, // display results (complicated) + ...props +}) => { + const [error, setError] = useState(""); + const [touched, setTouched] = useState(false); + const inputRef = useRef(null); + const cursorRef = useRef(0); + + useLayoutEffect(() => { + if (inputRef.current) { + inputRef.current.setSelectionRange(cursorRef.current, cursorRef.current); + } + }, [value]); + + useEffect(() => { + const hasError = !!error; + if (setErrorStatus) { + setErrorStatus(hasError); + } + }, [error, setErrorStatus]); + + useEffect(() => { + if (touched) { + const isValidFormat = + /^(\d{3}-\d{4}-\d{4})$/.test(value) || + /^\d*$/.test(value.replace(/-/g, "")); + + if (!value) { + setError("필수로 채워야 하는 항목입니다"); + } else if (!isValidFormat) { + setError("숫자만 입력 가능합니다"); + } else if ( + value.replace(/-/g, "").length !== 11 || + value.slice(0, 3) !== "010" + ) { + setError("유효하지 않은 전화번호입니다"); + } else { + setError(""); + } + } + }, [value, touched]); + + const handleBlur = () => { + setTouched(true); + }; + + const formatValue = (nums: string) => { + const digits = nums.replace(/\D/g, ""); + let formattedInput = ""; + + if (digits.length <= 3) { + formattedInput = digits; + } else if (digits.length <= 7) { + formattedInput = `${digits.slice(0, 3)}-${digits.slice(3)}`; + } else if (digits.length <= 11) { + formattedInput = `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`; + } + + return formattedInput; + }; + + const handlePhoneValueChange = (e: ChangeEvent) => { + const inputValue = e.target.value; + const currentCursor = inputRef.current?.selectionStart || 0; + const lengthDifference = inputValue.length - formatValue(value).length; + // console.log(currentCursor, lengthDifference); + if ( + lengthDifference > 0 && + (currentCursor === 3 || + currentCursor === 4 || + currentCursor === 8 || + currentCursor === 9) + ) { + cursorRef.current = currentCursor + 1; + } else if ( + lengthDifference < 0 && + ((currentCursor === 4 && inputValue.length === 4) || + (currentCursor === 9 && inputValue.length === 9)) + ) { + cursorRef.current = currentCursor - 1; + } else { + cursorRef.current = currentCursor; + } + if (inputValue.length <= 13) handleChange(inputValue); + }; + + return ( + + {label && } + + + {error && {error}} + + + ); +}; + +export default PhoneInput; From 7e6576e0bd2af5f870e4e7b5f3dd1ef98bce036a Mon Sep 17 00:00:00 2001 From: chacha Date: Mon, 30 Sep 2024 09:17:45 +0900 Subject: [PATCH 2/7] feat: add isprorvalid --- packages/web/package.json | 1 + pnpm-lock.yaml | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/web/package.json b/packages/web/package.json index 14e8f95..10385f3 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@emotion/cache": "11.11.0", + "@emotion/is-prop-valid": "^1.3.1", "@emotion/react": "11.11.4", "@emotion/styled": "11.11.0", "@mui/icons-material": "5.15.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be2573a..edac125 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -187,6 +187,9 @@ importers: '@emotion/cache': specifier: 11.11.0 version: 11.11.0 + '@emotion/is-prop-valid': + specifier: ^1.3.1 + version: 1.3.1 '@emotion/react': specifier: 11.11.4 version: 11.11.4(@types/react@18.2.64)(react@18.2.0) @@ -893,6 +896,12 @@ packages: '@emotion/memoize': 0.8.1 dev: false + /@emotion/is-prop-valid@1.3.1: + resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + dependencies: + '@emotion/memoize': 0.9.0 + dev: false + /@emotion/memoize@0.7.4: resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} requiresBuild: true @@ -903,6 +912,10 @@ packages: resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} dev: false + /@emotion/memoize@0.9.0: + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + dev: false + /@emotion/react@11.11.4(@types/react@18.2.64)(react@18.2.0): resolution: {integrity: sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==} peerDependencies: @@ -950,7 +963,7 @@ packages: dependencies: '@babel/runtime': 7.24.0 '@emotion/babel-plugin': 11.11.0 - '@emotion/is-prop-valid': 1.2.1 + '@emotion/is-prop-valid': 1.3.1 '@emotion/react': 11.11.4(@types/react@18.2.64)(react@18.2.0) '@emotion/serialize': 1.1.3 '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) From 05785f26f66ba773900df867998b54090e7f50d6 Mon Sep 17 00:00:00 2001 From: chacha Date: Mon, 30 Sep 2024 14:43:26 +0900 Subject: [PATCH 3/7] feat: change name, delete console log, delete area from PhoneInput --- .../common/components/Forms/NewPhoneInput.tsx | 215 ------------------ .../common/components/Forms/PhoneInput.tsx | 177 +++++++++++--- 2 files changed, 148 insertions(+), 244 deletions(-) delete mode 100644 packages/web/src/common/components/Forms/NewPhoneInput.tsx diff --git a/packages/web/src/common/components/Forms/NewPhoneInput.tsx b/packages/web/src/common/components/Forms/NewPhoneInput.tsx deleted file mode 100644 index ec53597..0000000 --- a/packages/web/src/common/components/Forms/NewPhoneInput.tsx +++ /dev/null @@ -1,215 +0,0 @@ -/* ---- 사용 방법 (예시) --- - -*/ - -import React, { - ChangeEvent, - ChangeEventHandler, - InputHTMLAttributes, - useEffect, - useLayoutEffect, - useRef, - useState, -} from "react"; - -import isPropValid from "@emotion/is-prop-valid"; -import styled, { css } from "styled-components"; - -import ErrorMessage from "./_atomic/ErrorMessage"; -import Label from "./_atomic/Label"; - -export interface PhoneInputProps - extends InputHTMLAttributes { - label?: string; - placeholder: string; - area?: boolean; - disabled?: boolean; - value?: string; - handleChange?: (value: string) => void; - setErrorStatus?: (hasError: boolean) => void; - onChange?: ChangeEventHandler; -} - -const errorBorderStyle = css` - border-color: ${({ theme }) => theme.colors.RED[600]}; -`; - -const disabledStyle = css` - background-color: ${({ theme }) => theme.colors.GRAY[100]}; - border-color: ${({ theme }) => theme.colors.GRAY[200]}; -`; - -const areaInputStyle = css` - height: 100px; - resize: none; - overflow: auto; -`; - -const Input = styled.input - .withConfig({ - shouldForwardProp: prop => - isPropValid(prop) && prop !== "hasError" && prop !== "area", - }) - .attrs(({ area }) => ({ - as: area ? "textarea" : "input", - }))` - display: block; - width: 100%; - padding: 8px 12px 8px 12px; - outline: none; - border: 1px solid ${({ theme }) => theme.colors.GRAY[200]}; - border-radius: 4px; - gap: 8px; - font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD}; - font-size: 16px; - line-height: 20px; - font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR}; - color: ${({ theme }) => theme.colors.BLACK}; - background-color: ${({ theme }) => theme.colors.WHITE}; - &:focus { - border-color: ${({ theme, hasError, disabled }) => - !hasError && !disabled && theme.colors.PRIMARY}; - } - &:hover:not(:focus) { - border-color: ${({ theme, hasError, disabled }) => - !hasError && !disabled && theme.colors.GRAY[300]}; - } - &::placeholder { - color: ${({ theme }) => theme.colors.GRAY[200]}; - } - ${({ disabled }) => disabled && disabledStyle} - ${({ hasError }) => hasError && errorBorderStyle} - ${({ area }) => area && areaInputStyle} // TextAreaInput -`; - -const InputWrapper = styled.div` - width: 100%; - flex-direction: column; - display: flex; - gap: 4px; -`; - -// Component -const PhoneInput: React.FC = ({ - label = "", - placeholder, - area = false, - disabled = false, - value = "", - handleChange = () => {}, // setValue - setErrorStatus = () => {}, - onChange = undefined, // display results (complicated) - ...props -}) => { - const [error, setError] = useState(""); - const [touched, setTouched] = useState(false); - const inputRef = useRef(null); - const cursorRef = useRef(0); - - useLayoutEffect(() => { - if (inputRef.current) { - inputRef.current.setSelectionRange(cursorRef.current, cursorRef.current); - } - }, [value]); - - useEffect(() => { - const hasError = !!error; - if (setErrorStatus) { - setErrorStatus(hasError); - } - }, [error, setErrorStatus]); - - useEffect(() => { - if (touched) { - const isValidFormat = - /^(\d{3}-\d{4}-\d{4})$/.test(value) || - /^\d*$/.test(value.replace(/-/g, "")); - - if (!value) { - setError("필수로 채워야 하는 항목입니다"); - } else if (!isValidFormat) { - setError("숫자만 입력 가능합니다"); - } else if ( - value.replace(/-/g, "").length !== 11 || - value.slice(0, 3) !== "010" - ) { - setError("유효하지 않은 전화번호입니다"); - } else { - setError(""); - } - } - }, [value, touched]); - - const handleBlur = () => { - setTouched(true); - }; - - const formatValue = (nums: string) => { - const digits = nums.replace(/\D/g, ""); - let formattedInput = ""; - - if (digits.length <= 3) { - formattedInput = digits; - } else if (digits.length <= 7) { - formattedInput = `${digits.slice(0, 3)}-${digits.slice(3)}`; - } else if (digits.length <= 11) { - formattedInput = `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`; - } - - return formattedInput; - }; - - const handlePhoneValueChange = (e: ChangeEvent) => { - const inputValue = e.target.value; - const currentCursor = inputRef.current?.selectionStart || 0; - const lengthDifference = inputValue.length - formatValue(value).length; - // console.log(currentCursor, lengthDifference); - if ( - lengthDifference > 0 && - (currentCursor === 3 || - currentCursor === 4 || - currentCursor === 8 || - currentCursor === 9) - ) { - cursorRef.current = currentCursor + 1; - } else if ( - lengthDifference < 0 && - ((currentCursor === 4 && inputValue.length === 4) || - (currentCursor === 9 && inputValue.length === 9)) - ) { - cursorRef.current = currentCursor - 1; - } else { - cursorRef.current = currentCursor; - } - if (inputValue.length <= 13) handleChange(inputValue); - }; - - return ( - - {label && } - - - {error && {error}} - - - ); -}; - -export default PhoneInput; diff --git a/packages/web/src/common/components/Forms/PhoneInput.tsx b/packages/web/src/common/components/Forms/PhoneInput.tsx index e4b4806..75afdb4 100644 --- a/packages/web/src/common/components/Forms/PhoneInput.tsx +++ b/packages/web/src/common/components/Forms/PhoneInput.tsx @@ -1,21 +1,116 @@ -import React, { ChangeEvent, useEffect, useState } from "react"; -import TextInput, { - TextInputProps, -} from "@sparcs-students/web/common/components/Forms/TextInput"; - -interface PhoneInputProps extends Omit { - value: string; - onChange: (value: string) => void; +/* +--- 사용 방법 (예시) --- + +*/ + +import React, { + ChangeEvent, + ChangeEventHandler, + InputHTMLAttributes, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; + +import isPropValid from "@emotion/is-prop-valid"; +import styled, { css } from "styled-components"; + +import ErrorMessage from "./_atomic/ErrorMessage"; +import Label from "./_atomic/Label"; + +export interface PhoneInputProps + extends InputHTMLAttributes { + label?: string; + placeholder: string; + disabled?: boolean; + value?: string; + handleChange?: (value: string) => void; + setErrorStatus?: (hasError: boolean) => void; + onChange?: ChangeEventHandler; } +const errorBorderStyle = css` + border-color: ${({ theme }) => theme.colors.RED[600]}; +`; + +const disabledStyle = css` + background-color: ${({ theme }) => theme.colors.GRAY[100]}; + border-color: ${({ theme }) => theme.colors.GRAY[200]}; +`; + +const Input = styled.input.withConfig({ + shouldForwardProp: prop => isPropValid(prop) && prop !== "hasError", +})` + display: block; + width: 100%; + padding: 8px 12px 8px 12px; + outline: none; + border: 1px solid ${({ theme }) => theme.colors.GRAY[200]}; + border-radius: 4px; + gap: 8px; + font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD}; + font-size: 16px; + line-height: 20px; + font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR}; + color: ${({ theme }) => theme.colors.BLACK}; + background-color: ${({ theme }) => theme.colors.WHITE}; + &:focus { + border-color: ${({ theme, hasError, disabled }) => + !hasError && !disabled && theme.colors.PRIMARY}; + } + &:hover:not(:focus) { + border-color: ${({ theme, hasError, disabled }) => + !hasError && !disabled && theme.colors.GRAY[300]}; + } + &::placeholder { + color: ${({ theme }) => theme.colors.GRAY[200]}; + } + ${({ disabled }) => disabled && disabledStyle} + ${({ hasError }) => hasError && errorBorderStyle} +`; + +const InputWrapper = styled.div` + width: 100%; + flex-direction: column; + display: flex; + gap: 4px; +`; + +// Component const PhoneInput: React.FC = ({ label = "", + placeholder, + disabled = false, value = "", - onChange = () => {}, + handleChange = () => {}, // setValue + setErrorStatus = () => {}, + onChange = undefined, // display results (complicated) ...props }) => { const [error, setError] = useState(""); const [touched, setTouched] = useState(false); + const inputRef = useRef(null); + const cursorRef = useRef(0); + + useLayoutEffect(() => { + if (inputRef.current) { + inputRef.current.setSelectionRange(cursorRef.current, cursorRef.current); + } + }, [value]); + + useEffect(() => { + const hasError = !!error; + if (setErrorStatus) { + setErrorStatus(hasError); + } + }, [error, setErrorStatus]); useEffect(() => { if (touched) { @@ -27,7 +122,10 @@ const PhoneInput: React.FC = ({ setError("필수로 채워야 하는 항목입니다"); } else if (!isValidFormat) { setError("숫자만 입력 가능합니다"); - } else if (value.replace(/-/g, "").length !== 11) { + } else if ( + value.replace(/-/g, "").length !== 11 || + value.slice(0, 3) !== "010" + ) { setError("유효하지 않은 전화번호입니다"); } else { setError(""); @@ -39,16 +137,6 @@ const PhoneInput: React.FC = ({ setTouched(true); }; - const handleChange = ( - e: ChangeEvent, - ) => { - const inputValue = e.target.value; - - if (inputValue.length <= 13) { - onChange(inputValue); - } - }; - const formatValue = (nums: string) => { const digits = nums.replace(/\D/g, ""); let formattedInput = ""; @@ -64,17 +152,48 @@ const PhoneInput: React.FC = ({ return formattedInput; }; + const handlePhoneValueChange = (e: ChangeEvent) => { + const inputValue = e.target.value; + const currentCursor = inputRef.current?.selectionStart || 0; + const lengthDifference = inputValue.length - formatValue(value).length; + if ( + lengthDifference > 0 && + (currentCursor === 3 || + currentCursor === 4 || + currentCursor === 8 || + currentCursor === 9) + ) { + cursorRef.current = currentCursor + 1; + } else if ( + lengthDifference < 0 && + ((currentCursor === 4 && inputValue.length === 4) || + (currentCursor === 9 && inputValue.length === 9)) + ) { + cursorRef.current = currentCursor - 1; + } else { + cursorRef.current = currentCursor; + } + if (inputValue.length <= 13) handleChange(inputValue); + }; + return ( - + + {label && } + + + {error && {error}} + + ); }; -// TODO: DB 값 default로 넣어두기 export default PhoneInput; From 360e075ec984b671ac21e8cbd4b9d0e1565118e3 Mon Sep 17 00:00:00 2001 From: chacha Date: Mon, 30 Sep 2024 20:01:31 +0900 Subject: [PATCH 4/7] feat: use react-hook-form in PhoneInput --- packages/web/package.json | 1 + .../common/components/Forms/PhoneInput.tsx | 80 +++++++++++-------- pnpm-lock.yaml | 12 +++ 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/packages/web/package.json b/packages/web/package.json index 10385f3..65de8c5 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -30,6 +30,7 @@ "next": "14.1.3", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.53.0", "styled-components": "^6.1.8", "zod": "^3.21.4", "zustand": "^4.5.2" diff --git a/packages/web/src/common/components/Forms/PhoneInput.tsx b/packages/web/src/common/components/Forms/PhoneInput.tsx index 75afdb4..2e91053 100644 --- a/packages/web/src/common/components/Forms/PhoneInput.tsx +++ b/packages/web/src/common/components/Forms/PhoneInput.tsx @@ -1,6 +1,6 @@ /* --- 사용 방법 (예시) --- - { label?: string; placeholder: string; - disabled?: boolean; value?: string; handleChange?: (value: string) => void; - setErrorStatus?: (hasError: boolean) => void; onChange?: ChangeEventHandler; } @@ -84,17 +85,23 @@ const InputWrapper = styled.div` `; // Component -const PhoneInput: React.FC = ({ +const PhoneInput2: React.FC< + PhoneInputProps & { setErrorStatus?: (hasError: boolean) => void } +> = ({ label = "", placeholder, disabled = false, value = "", handleChange = () => {}, // setValue - setErrorStatus = () => {}, onChange = undefined, // display results (complicated) + setErrorStatus = () => {}, ...props }) => { - const [error, setError] = useState(""); + const formCtx = useForm({ + defaultValues: { + phoneNumber: "", + }, + }); const [touched, setTouched] = useState(false); const inputRef = useRef(null); const cursorRef = useRef(0); @@ -105,13 +112,6 @@ const PhoneInput: React.FC = ({ } }, [value]); - useEffect(() => { - const hasError = !!error; - if (setErrorStatus) { - setErrorStatus(hasError); - } - }, [error, setErrorStatus]); - useEffect(() => { if (touched) { const isValidFormat = @@ -119,19 +119,26 @@ const PhoneInput: React.FC = ({ /^\d*$/.test(value.replace(/-/g, "")); if (!value) { - setError("필수로 채워야 하는 항목입니다"); + formCtx.setError("phoneNumber", { + message: "필수로 채워야 하는 항목입니다", + }); } else if (!isValidFormat) { - setError("숫자만 입력 가능합니다"); + formCtx.setError("phoneNumber", { + message: "숫자만 입력 가능합니다", + }); } else if ( value.replace(/-/g, "").length !== 11 || value.slice(0, 3) !== "010" ) { - setError("유효하지 않은 전화번호입니다"); + formCtx.setError("phoneNumber", { + message: "유효하지 않은 전화번호입니다", + }); } else { - setError(""); + formCtx.clearErrors("phoneNumber"); } + setErrorStatus(!!formCtx.formState.errors.phoneNumber); } - }, [value, touched]); + }, [value, touched, formCtx]); const handleBlur = () => { setTouched(true); @@ -180,20 +187,29 @@ const PhoneInput: React.FC = ({ {label && } - - {error && {error}} + + + {formCtx.formState.errors.phoneNumber && ( + + {formCtx.formState.errors.phoneNumber.message} + + )} + ); }; -export default PhoneInput; +export default PhoneInput2; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index edac125..62d22f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -238,6 +238,9 @@ importers: react-dom: specifier: ^18 version: 18.2.0(react@18.2.0) + react-hook-form: + specifier: ^7.53.0 + version: 7.53.0(react@18.2.0) styled-components: specifier: ^6.1.8 version: 6.1.8(react-dom@18.2.0)(react@18.2.0) @@ -7610,6 +7613,15 @@ packages: scheduler: 0.23.0 dev: false + /react-hook-form@7.53.0(react@18.2.0): + resolution: {integrity: sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + dependencies: + react: 18.2.0 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} From a2f6a0afcce1fea892ef761bea34688b0c96f989 Mon Sep 17 00:00:00 2001 From: chacha Date: Mon, 30 Sep 2024 21:56:21 +0900 Subject: [PATCH 5/7] feat: add optional props to PhoneInput --- packages/web/src/common/components/Forms/PhoneInput.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/web/src/common/components/Forms/PhoneInput.tsx b/packages/web/src/common/components/Forms/PhoneInput.tsx index 2e91053..f6a0654 100644 --- a/packages/web/src/common/components/Forms/PhoneInput.tsx +++ b/packages/web/src/common/components/Forms/PhoneInput.tsx @@ -86,7 +86,10 @@ const InputWrapper = styled.div` // Component const PhoneInput2: React.FC< - PhoneInputProps & { setErrorStatus?: (hasError: boolean) => void } + PhoneInputProps & { + setErrorStatus?: (hasError: boolean) => void; + optional?: boolean; + } > = ({ label = "", placeholder, @@ -95,6 +98,7 @@ const PhoneInput2: React.FC< handleChange = () => {}, // setValue onChange = undefined, // display results (complicated) setErrorStatus = () => {}, + optional = false, ...props }) => { const formCtx = useForm({ @@ -118,7 +122,7 @@ const PhoneInput2: React.FC< /^(\d{3}-\d{4}-\d{4})$/.test(value) || /^\d*$/.test(value.replace(/-/g, "")); - if (!value) { + if (!optional && !value) { formCtx.setError("phoneNumber", { message: "필수로 채워야 하는 항목입니다", }); From f40977a4928bb611404ae4b1e26ab7136ca21f2c Mon Sep 17 00:00:00 2001 From: chacha Date: Mon, 30 Sep 2024 22:55:50 +0900 Subject: [PATCH 6/7] feat: undo using react-hook-form --- .../common/components/Forms/PhoneInput.tsx | 89 +++++++------------ 1 file changed, 33 insertions(+), 56 deletions(-) diff --git a/packages/web/src/common/components/Forms/PhoneInput.tsx b/packages/web/src/common/components/Forms/PhoneInput.tsx index f6a0654..ae9bcd9 100644 --- a/packages/web/src/common/components/Forms/PhoneInput.tsx +++ b/packages/web/src/common/components/Forms/PhoneInput.tsx @@ -1,6 +1,6 @@ /* --- 사용 방법 (예시) --- - { label?: string; placeholder: string; + disabled?: boolean; value?: string; handleChange?: (value: string) => void; + setErrorStatus?: (hasError: boolean) => void; onChange?: ChangeEventHandler; } @@ -85,27 +84,17 @@ const InputWrapper = styled.div` `; // Component -const PhoneInput2: React.FC< - PhoneInputProps & { - setErrorStatus?: (hasError: boolean) => void; - optional?: boolean; - } -> = ({ +const PhoneInput: React.FC = ({ label = "", placeholder, disabled = false, value = "", handleChange = () => {}, // setValue - onChange = undefined, // display results (complicated) setErrorStatus = () => {}, - optional = false, + onChange = undefined, // display results (complicated) ...props }) => { - const formCtx = useForm({ - defaultValues: { - phoneNumber: "", - }, - }); + const [error, setError] = useState(""); const [touched, setTouched] = useState(false); const inputRef = useRef(null); const cursorRef = useRef(0); @@ -116,33 +105,32 @@ const PhoneInput2: React.FC< } }, [value]); + useEffect(() => { + const hasError = !!error; + if (setErrorStatus) { + setErrorStatus(hasError); + } + }, [error, setErrorStatus]); + useEffect(() => { if (touched) { const isValidFormat = /^(\d{3}-\d{4}-\d{4})$/.test(value) || /^\d*$/.test(value.replace(/-/g, "")); - - if (!optional && !value) { - formCtx.setError("phoneNumber", { - message: "필수로 채워야 하는 항목입니다", - }); + if (!value) { + setError("필수로 채워야 하는 항목입니다"); } else if (!isValidFormat) { - formCtx.setError("phoneNumber", { - message: "숫자만 입력 가능합니다", - }); + setError("숫자만 입력 가능합니다"); } else if ( value.replace(/-/g, "").length !== 11 || value.slice(0, 3) !== "010" ) { - formCtx.setError("phoneNumber", { - message: "유효하지 않은 전화번호입니다", - }); + setError("유효하지 않은 전화번호입니다"); } else { - formCtx.clearErrors("phoneNumber"); + setError(""); } - setErrorStatus(!!formCtx.formState.errors.phoneNumber); } - }, [value, touched, formCtx]); + }, [value, touched]); const handleBlur = () => { setTouched(true); @@ -151,7 +139,6 @@ const PhoneInput2: React.FC< const formatValue = (nums: string) => { const digits = nums.replace(/\D/g, ""); let formattedInput = ""; - if (digits.length <= 3) { formattedInput = digits; } else if (digits.length <= 7) { @@ -159,7 +146,6 @@ const PhoneInput2: React.FC< } else if (digits.length <= 11) { formattedInput = `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`; } - return formattedInput; }; @@ -191,29 +177,20 @@ const PhoneInput2: React.FC< {label && } - - - {formCtx.formState.errors.phoneNumber && ( - - {formCtx.formState.errors.phoneNumber.message} - - )} - + + {error && {error}} ); }; -export default PhoneInput2; +export default PhoneInput; From bdfa0e9e07d1073d1e3581edc23852d5efdacd8b Mon Sep 17 00:00:00 2001 From: chacha Date: Mon, 30 Sep 2024 23:11:34 +0900 Subject: [PATCH 7/7] feat: add optional to PhoneInput --- packages/web/src/common/components/Forms/PhoneInput.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/web/src/common/components/Forms/PhoneInput.tsx b/packages/web/src/common/components/Forms/PhoneInput.tsx index ae9bcd9..c4c1e25 100644 --- a/packages/web/src/common/components/Forms/PhoneInput.tsx +++ b/packages/web/src/common/components/Forms/PhoneInput.tsx @@ -84,7 +84,7 @@ const InputWrapper = styled.div` `; // Component -const PhoneInput: React.FC = ({ +const PhoneInput: React.FC = ({ label = "", placeholder, disabled = false, @@ -92,6 +92,7 @@ const PhoneInput: React.FC = ({ handleChange = () => {}, // setValue setErrorStatus = () => {}, onChange = undefined, // display results (complicated) + optional = false, ...props }) => { const [error, setError] = useState(""); @@ -117,7 +118,7 @@ const PhoneInput: React.FC = ({ const isValidFormat = /^(\d{3}-\d{4}-\d{4})$/.test(value) || /^\d*$/.test(value.replace(/-/g, "")); - if (!value) { + if (!optional && !value) { setError("필수로 채워야 하는 항목입니다"); } else if (!isValidFormat) { setError("숫자만 입력 가능합니다");