diff --git a/package.json b/package.json index c19769c..71381bc 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react-dom": "^19.0.0", "react-mobile-picker": "^1.1.2", "react-qr-code": "^2.0.15", + "yup": "^1.6.1", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ab76d0..dbaff68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: react-qr-code: specifier: ^2.0.15 version: 2.0.15(react@19.1.0) + yup: + specifier: ^1.6.1 + version: 1.6.1 zustand: specifier: ^5.0.3 version: 5.0.4(@types/react@19.1.4)(react@19.1.0) @@ -3896,6 +3899,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -4357,6 +4363,9 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -4397,6 +4406,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -4713,6 +4725,9 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} + yup@1.6.1: + resolution: {integrity: sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==} + zustand@5.0.4: resolution: {integrity: sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==} engines: {node: '>=12.20.0'} @@ -8730,6 +8745,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-expr@2.0.6: {} + proto-list@1.2.4: {} proxy-agent@6.5.0: @@ -9314,6 +9331,8 @@ snapshots: glob: 10.4.5 minimatch: 9.0.5 + tiny-case@1.0.3: {} + tiny-invariant@1.3.3: {} tiny-warning@1.0.3: {} @@ -9343,6 +9362,8 @@ snapshots: dependencies: is-number: 7.0.0 + toposort@2.0.2: {} + totalist@3.0.1: {} tree-kill@1.2.2: {} @@ -9703,6 +9724,13 @@ snapshots: yoctocolors-cjs@2.1.2: {} + yup@1.6.1: + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 + zustand@5.0.4(@types/react@19.1.4)(react@19.1.0): optionalDependencies: '@types/react': 19.1.4 diff --git a/src/app/(auth)/join/[type]/[step]/_components/ParentStep01.tsx b/src/app/(auth)/join/[type]/[step]/_components/ParentStep01.tsx index 18a172b..c47b801 100644 --- a/src/app/(auth)/join/[type]/[step]/_components/ParentStep01.tsx +++ b/src/app/(auth)/join/[type]/[step]/_components/ParentStep01.tsx @@ -14,14 +14,52 @@ const ParentStep01 = () => { const router = useRouter() const { updateParent } = useUserStore() const [nickname, setNickname] = useState("") + const [error, setError] = useState("") + + // 닉네임 유효성 검증 함수 + const validateNickname = (value: string) => { + // 빈 값 체크 + if (!value.trim()) { + return "닉네임을 입력해주세요" + } + + // 길이 체크 + if (value.trim().length < 2) { + return "닉네임은 최소 2자 이상이어야 합니다" + } + + if (value.trim().length > 20) { + return "닉네임은 최대 20자까지 입력 가능합니다" + } + + // 한글 + 영문 + 숫자 + 언더스코어 + 하이픈만 허용 + const nicknameRegex = /^[가-힣a-zA-Z0-9_-]+$/ + if (!nicknameRegex.test(value.trim())) { + return "닉네임은 한글, 영문, 숫자, _, - 만 사용 가능합니다" + } + + return "" + } const handleChange = (e: React.ChangeEvent) => { - setNickname(e.target.value) + const value = e.target.value + setNickname(value) + + // 실시간 검증 (선택사항) + const validationError = validateNickname(value) + setError(validationError) } const handleClickNext = () => { + const validationError = validateNickname(nickname) + + if (validationError) { + setError(validationError) + return + } + updateParent({ - nickname: nickname, + nickname: nickname.trim(), }) router.push("/join/parent/2") } @@ -38,6 +76,14 @@ const ParentStep01 = () => { placeholder="닉네임 입력" onChange={handleChange} value={nickname} + state={ + error + ? { + type: "error", + message: error, + } + : null + } />