From af3fa26f3b3a17bc47b809715bfdd65132a25e76 Mon Sep 17 00:00:00 2001 From: geonhee-min Date: Tue, 2 Dec 2025 16:50:24 +0900 Subject: [PATCH] =?UTF-8?q?issue=20#37=20-=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=201=EC=B0=A8=20=EC=99=84=EB=A3=8C(=EB=8F=99?= =?UTF-8?q?=EC=9E=91=20=ED=99=95=EC=9D=B8=20=ED=95=84=EC=9A=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components.json | 4 +- package-lock.json | 8 +- package.json | 2 +- src/App.tsx | 6 +- src/components/ui/stepper.tsx | 408 ++++++++++++++++++ src/data/form/resetPassword.schema.ts | 15 + src/data/form/signup.schema.ts | 2 +- .../request/account/ResetPasswordRequest.ts | 4 + .../account/SendResetPasswordCodeRequest.ts | 3 + .../account/VerifyResetPasswordCodeRequest.ts | 4 + src/data/request/index.ts | 5 +- src/data/response/BaseResponse.ts | 1 + src/data/response/account/LoginResponse.ts | 1 - .../response/account/ResetPasswordResponse.ts | 5 + .../account/SendResetPasswordCodeResponse.ts | 5 + .../account/SendVerificationCodeResponse.ts | 2 +- src/data/response/account/SignupResponse.ts | 1 - .../VerifyResetPasswordCodeResponse.ts | 5 + src/data/response/index.ts | 5 +- src/network/AccountNetwork.ts | 31 +- src/ui/page/{ => account}/login/LoginPage.tsx | 0 .../page/{ => account}/signup/SignUpPage.tsx | 0 .../page/resetPassword/ResetPasswordPage.tsx | 76 ---- src/util/Validator.ts | 23 +- 24 files changed, 519 insertions(+), 97 deletions(-) create mode 100644 src/components/ui/stepper.tsx create mode 100644 src/data/request/account/ResetPasswordRequest.ts create mode 100644 src/data/request/account/SendResetPasswordCodeRequest.ts create mode 100644 src/data/request/account/VerifyResetPasswordCodeRequest.ts create mode 100644 src/data/response/account/ResetPasswordResponse.ts create mode 100644 src/data/response/account/SendResetPasswordCodeResponse.ts create mode 100644 src/data/response/account/VerifyResetPasswordCodeResponse.ts rename src/ui/page/{ => account}/login/LoginPage.tsx (100%) rename src/ui/page/{ => account}/signup/SignUpPage.tsx (100%) delete mode 100644 src/ui/page/resetPassword/ResetPasswordPage.tsx diff --git a/components.json b/components.json index ffd5afa..f892854 100644 --- a/components.json +++ b/components.json @@ -18,5 +18,7 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "registries": {} + "registries": { + "@reui": "https://reui.io/r/{name}.json" + } } diff --git a/package-lock.json b/package-lock.json index d88a828..0d54129 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "react-router-dom": "^7.9.5", "recharts": "^2.15.4", "sonner": "^2.0.7", - "tailwind-merge": "^3.3.1", + "tailwind-merge": "^3.4.0", "vaul": "^1.1.2", "zod": "^4.1.12", "zustand": "^5.0.8" @@ -6376,9 +6376,9 @@ } }, "node_modules/tailwind-merge": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", - "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", "license": "MIT", "funding": { "type": "github", diff --git a/package.json b/package.json index 6785f6c..0254749 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "react-router-dom": "^7.9.5", "recharts": "^2.15.4", "sonner": "^2.0.7", - "tailwind-merge": "^3.3.1", + "tailwind-merge": "^3.4.0", "vaul": "^1.1.2", "zod": "^4.1.12", "zustand": "^5.0.8" diff --git a/src/App.tsx b/src/App.tsx index 28de1e2..4ae56a9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,11 @@ import './App.css'; -import SignUpPage from './ui/page/signup/SignUpPage'; +import SignUpPage from './ui/page/account/signup/SignUpPage'; import Layout from './layouts/Layout'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { useAuthStore } from './store/authStore'; import { PageRouting } from './const/PageRouting'; -import LoginPage from './ui/page/login/LoginPage'; -import ResetPasswordPage from './ui/page/resetPassword/ResetPasswordPage'; +import LoginPage from './ui/page/account/login/LoginPage'; +import ResetPasswordPage from './ui/page/account/resetPassword/ResetPasswordPage'; function App() { const { authData } = useAuthStore(); diff --git a/src/components/ui/stepper.tsx b/src/components/ui/stepper.tsx new file mode 100644 index 0000000..d56feed --- /dev/null +++ b/src/components/ui/stepper.tsx @@ -0,0 +1,408 @@ +'use client'; + +import * as React from 'react'; +import { createContext, useContext } from 'react'; +import { cn } from '@/lib/utils'; + +// Types +type StepperOrientation = 'horizontal' | 'vertical'; +type StepState = 'active' | 'completed' | 'inactive' | 'loading'; +type StepIndicators = { + active?: React.ReactNode; + completed?: React.ReactNode; + inactive?: React.ReactNode; + loading?: React.ReactNode; +}; + +interface StepperContextValue { + activeStep: number; + setActiveStep: (step: number) => void; + stepsCount: number; + orientation: StepperOrientation; + registerTrigger: (node: HTMLButtonElement | null) => void; + triggerNodes: HTMLButtonElement[]; + focusNext: (currentIdx: number) => void; + focusPrev: (currentIdx: number) => void; + focusFirst: () => void; + focusLast: () => void; + indicators: StepIndicators; +} + +interface StepItemContextValue { + step: number; + state: StepState; + isDisabled: boolean; + isLoading: boolean; +} + +const StepperContext = createContext(undefined); +const StepItemContext = createContext(undefined); + +function useStepper() { + const ctx = useContext(StepperContext); + if (!ctx) throw new Error('useStepper must be used within a Stepper'); + return ctx; +} + +function useStepItem() { + const ctx = useContext(StepItemContext); + if (!ctx) throw new Error('useStepItem must be used within a StepperItem'); + return ctx; +} + +interface StepperProps extends React.HTMLAttributes { + defaultValue?: number; + value?: number; + onValueChange?: (value: number) => void; + orientation?: StepperOrientation; + indicators?: StepIndicators; +} + +function Stepper({ + defaultValue = 1, + value, + onValueChange, + orientation = 'horizontal', + className, + children, + indicators = {}, + ...props +}: StepperProps) { + const [activeStep, setActiveStep] = React.useState(defaultValue); + const [triggerNodes, setTriggerNodes] = React.useState([]); + + // Register/unregister triggers + const registerTrigger = React.useCallback((node: HTMLButtonElement | null) => { + setTriggerNodes((prev) => { + if (node && !prev.includes(node)) { + return [...prev, node]; + } else if (!node && prev.includes(node!)) { + return prev.filter((n) => n !== node); + } else { + return prev; + } + }); + }, []); + + const handleSetActiveStep = React.useCallback( + (step: number) => { + if (value === undefined) { + setActiveStep(step); + } + onValueChange?.(step); + }, + [value, onValueChange], + ); + + const currentStep = value ?? activeStep; + + // Keyboard navigation logic + const focusTrigger = (idx: number) => { + if (triggerNodes[idx]) triggerNodes[idx].focus(); + }; + const focusNext = (currentIdx: number) => focusTrigger((currentIdx + 1) % triggerNodes.length); + const focusPrev = (currentIdx: number) => focusTrigger((currentIdx - 1 + triggerNodes.length) % triggerNodes.length); + const focusFirst = () => focusTrigger(0); + const focusLast = () => focusTrigger(triggerNodes.length - 1); + + // Context value + const contextValue = React.useMemo( + () => ({ + activeStep: currentStep, + setActiveStep: handleSetActiveStep, + stepsCount: React.Children.toArray(children).filter( + (child): child is React.ReactElement => + React.isValidElement(child) && (child.type as { displayName?: string }).displayName === 'StepperItem', + ).length, + orientation, + registerTrigger, + focusNext, + focusPrev, + focusFirst, + focusLast, + triggerNodes, + indicators, + }), + [currentStep, handleSetActiveStep, children, orientation, registerTrigger, triggerNodes], + ); + + return ( + +
+ {children} +
+
+ ); +} + +interface StepperItemProps extends React.HTMLAttributes { + step: number; + completed?: boolean; + disabled?: boolean; + loading?: boolean; +} + +function StepperItem({ + step, + completed = false, + disabled = false, + loading = false, + className, + children, + ...props +}: StepperItemProps) { + const { activeStep } = useStepper(); + + const state: StepState = completed || step < activeStep ? 'completed' : activeStep === step ? 'active' : 'inactive'; + + const isLoading = loading && step === activeStep; + + return ( + +
+ {children} +
+
+ ); +} + +interface StepperTriggerProps extends React.ButtonHTMLAttributes { + asChild?: boolean; +} + +function StepperTrigger({ asChild = false, className, children, tabIndex, ...props }: StepperTriggerProps) { + const { state, isLoading } = useStepItem(); + const stepperCtx = useStepper(); + const { setActiveStep, activeStep, registerTrigger, triggerNodes, focusNext, focusPrev, focusFirst, focusLast } = + stepperCtx; + const { step, isDisabled } = useStepItem(); + const isSelected = activeStep === step; + const id = `stepper-tab-${step}`; + const panelId = `stepper-panel-${step}`; + + // Register this trigger for keyboard navigation + const btnRef = React.useRef(null); + React.useEffect(() => { + if (btnRef.current) { + registerTrigger(btnRef.current); + } + }, [btnRef.current]); + + // Find our index among triggers for navigation + const myIdx = React.useMemo( + () => triggerNodes.findIndex((n: HTMLButtonElement) => n === btnRef.current), + [triggerNodes, btnRef.current], + ); + + const handleKeyDown = (e: React.KeyboardEvent) => { + switch (e.key) { + case 'ArrowRight': + case 'ArrowDown': + e.preventDefault(); + if (myIdx !== -1 && focusNext) focusNext(myIdx); + break; + case 'ArrowLeft': + case 'ArrowUp': + e.preventDefault(); + if (myIdx !== -1 && focusPrev) focusPrev(myIdx); + break; + case 'Home': + e.preventDefault(); + if (focusFirst) focusFirst(); + break; + case 'End': + e.preventDefault(); + if (focusLast) focusLast(); + break; + case 'Enter': + case ' ': + e.preventDefault(); + setActiveStep(step); + break; + } + }; + + if (asChild) { + return ( + + {children} + + ); + } + + return ( + + ); +} + +function StepperIndicator({ children, className }: React.ComponentProps<'div'>) { + const { state, isLoading } = useStepItem(); + const { indicators } = useStepper(); + + return ( +
+
+ {indicators && + ((isLoading && indicators.loading) || + (state === 'completed' && indicators.completed) || + (state === 'active' && indicators.active) || + (state === 'inactive' && indicators.inactive)) + ? (isLoading && indicators.loading) || + (state === 'completed' && indicators.completed) || + (state === 'active' && indicators.active) || + (state === 'inactive' && indicators.inactive) + : children} +
+
+ ); +} + +function StepperSeparator({ className }: React.ComponentProps<'div'>) { + const { state } = useStepItem(); + + return ( +
+ ); +} + +function StepperTitle({ children, className }: React.ComponentProps<'h3'>) { + const { state } = useStepItem(); + + return ( +

+ {children} +

+ ); +} + +function StepperDescription({ children, className }: React.ComponentProps<'div'>) { + const { state } = useStepItem(); + + return ( +
+ {children} +
+ ); +} + +function StepperNav({ children, className }: React.ComponentProps<'nav'>) { + const { activeStep, orientation } = useStepper(); + + return ( + + ); +} + +function StepperPanel({ children, className }: React.ComponentProps<'div'>) { + const { activeStep } = useStepper(); + + return ( +
+ {children} +
+ ); +} + +interface StepperContentProps extends React.ComponentProps<'div'> { + value: number; + forceMount?: boolean; +} + +function StepperContent({ value, forceMount, children, className }: StepperContentProps) { + const { activeStep } = useStepper(); + const isActive = value === activeStep; + + if (!forceMount && !isActive) { + return null; + } + + return ( + + ); +} + +export { + useStepper, + useStepItem, + Stepper, + StepperItem, + StepperTrigger, + StepperIndicator, + StepperSeparator, + StepperTitle, + StepperDescription, + StepperPanel, + StepperContent, + StepperNav, + type StepperProps, + type StepperItemProps, + type StepperTriggerProps, + type StepperContentProps, +}; diff --git a/src/data/form/resetPassword.schema.ts b/src/data/form/resetPassword.schema.ts index 73360b3..6b6387f 100644 --- a/src/data/form/resetPassword.schema.ts +++ b/src/data/form/resetPassword.schema.ts @@ -3,4 +3,19 @@ import * as z from 'zod'; export const ResetPasswordSchema = z.object({ email: z .email() + , code: z + .string() + .length(8) + .regex(/^[a-z](?=.*[0-9])(?=.*[!@#$%^]).*$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.") + , password: z + .string() + .min(8, "비밀번호는 8-12 자리여야 합니다.") + .max(12, "비밀번호는 8-12 자리여야 합니다.") + .regex(/^[a-z](?=.*[0-9])(?=.*[!@#$%^]).*$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.") + , passwordConfirm: z + .string() +}) +.refine((data) => data.password === data.passwordConfirm, { + path: ["passwordConfirm"], + error: "비밀번호가 일치하지 않습니다." }); \ No newline at end of file diff --git a/src/data/form/signup.schema.ts b/src/data/form/signup.schema.ts index 2ed5766..eb52cf4 100644 --- a/src/data/form/signup.schema.ts +++ b/src/data/form/signup.schema.ts @@ -11,7 +11,7 @@ export const SignUpSchema = z.object({ .string() .min(8, "비밀번호는 8-12 자리여야 합니다.") .max(12, "비밀번호는 8-12 자리여야 합니다.") - .regex(/^[a-z](?=.*[0-9])(?=.*[!@#$]).*$/, "영문 소문자로 시작하고 숫자와 특수문자(!@#$)를 포함해야 합니다.") + .regex(/^[a-z](?=.*[0-9])(?=.*[!@#$%^]).*$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.") , name: z .string() .min(1, "이름을 입력해주시십시오.") diff --git a/src/data/request/account/ResetPasswordRequest.ts b/src/data/request/account/ResetPasswordRequest.ts new file mode 100644 index 0000000..ebe9fd3 --- /dev/null +++ b/src/data/request/account/ResetPasswordRequest.ts @@ -0,0 +1,4 @@ +export class ResetPasswordRequest { + email!: string; + password!: string; +} \ No newline at end of file diff --git a/src/data/request/account/SendResetPasswordCodeRequest.ts b/src/data/request/account/SendResetPasswordCodeRequest.ts new file mode 100644 index 0000000..769df64 --- /dev/null +++ b/src/data/request/account/SendResetPasswordCodeRequest.ts @@ -0,0 +1,3 @@ +export class SendResetPasswordCodeRequest { + email!: string; +} \ No newline at end of file diff --git a/src/data/request/account/VerifyResetPasswordCodeRequest.ts b/src/data/request/account/VerifyResetPasswordCodeRequest.ts new file mode 100644 index 0000000..227584d --- /dev/null +++ b/src/data/request/account/VerifyResetPasswordCodeRequest.ts @@ -0,0 +1,4 @@ +export class VerifyResetPasswordCodeRequest { + email!: string; + code!: string; +} \ No newline at end of file diff --git a/src/data/request/index.ts b/src/data/request/index.ts index 1663b31..9482f4a 100644 --- a/src/data/request/index.ts +++ b/src/data/request/index.ts @@ -2,4 +2,7 @@ export * from './account/CheckDuplicationRequest'; export * from './account/SendVerificationCodeRequest'; export * from './account/VerifyCodeRequest'; export * from './account/SignupRequest'; -export * from './account/LoginRequest'; \ No newline at end of file +export * from './account/LoginRequest'; +export * from './account/SendResetPasswordCodeRequest'; +export * from './account/VerifyResetPasswordCodeRequest'; +export * from './account/ResetPasswordRequest'; \ No newline at end of file diff --git a/src/data/response/BaseResponse.ts b/src/data/response/BaseResponse.ts index fe27c26..3d87020 100644 --- a/src/data/response/BaseResponse.ts +++ b/src/data/response/BaseResponse.ts @@ -1,4 +1,5 @@ export class BaseResponse { + success!: boolean; message?: string; error?: string; } \ No newline at end of file diff --git a/src/data/response/account/LoginResponse.ts b/src/data/response/account/LoginResponse.ts index 2eacb89..d2c9e62 100644 --- a/src/data/response/account/LoginResponse.ts +++ b/src/data/response/account/LoginResponse.ts @@ -1,7 +1,6 @@ import { BaseResponse } from "../BaseResponse"; export class LoginResponse extends BaseResponse { - success!: boolean; accessToken?: string; refreshToken?: string; } \ No newline at end of file diff --git a/src/data/response/account/ResetPasswordResponse.ts b/src/data/response/account/ResetPasswordResponse.ts new file mode 100644 index 0000000..619f280 --- /dev/null +++ b/src/data/response/account/ResetPasswordResponse.ts @@ -0,0 +1,5 @@ +import { BaseResponse } from "../BaseResponse"; + +export class ResetPasswordResponse extends BaseResponse { + +} \ No newline at end of file diff --git a/src/data/response/account/SendResetPasswordCodeResponse.ts b/src/data/response/account/SendResetPasswordCodeResponse.ts new file mode 100644 index 0000000..bfc2855 --- /dev/null +++ b/src/data/response/account/SendResetPasswordCodeResponse.ts @@ -0,0 +1,5 @@ +import { BaseResponse } from "../BaseResponse"; + +export class SendResetPasswordCodeResponse extends BaseResponse { + +} \ No newline at end of file diff --git a/src/data/response/account/SendVerificationCodeResponse.ts b/src/data/response/account/SendVerificationCodeResponse.ts index 9a7d6f8..0cbea4f 100644 --- a/src/data/response/account/SendVerificationCodeResponse.ts +++ b/src/data/response/account/SendVerificationCodeResponse.ts @@ -1,5 +1,5 @@ import { BaseResponse } from "../BaseResponse"; export class SendVerificationCodeResponse extends BaseResponse { - success!: boolean; + } \ No newline at end of file diff --git a/src/data/response/account/SignupResponse.ts b/src/data/response/account/SignupResponse.ts index f801ba1..38538e9 100644 --- a/src/data/response/account/SignupResponse.ts +++ b/src/data/response/account/SignupResponse.ts @@ -1,6 +1,5 @@ import { BaseResponse } from "../BaseResponse"; export class SignupResponse extends BaseResponse { - success!: boolean; } \ No newline at end of file diff --git a/src/data/response/account/VerifyResetPasswordCodeResponse.ts b/src/data/response/account/VerifyResetPasswordCodeResponse.ts new file mode 100644 index 0000000..49ffb23 --- /dev/null +++ b/src/data/response/account/VerifyResetPasswordCodeResponse.ts @@ -0,0 +1,5 @@ +import { BaseResponse } from "../BaseResponse"; + +export class VerifyResetPasswordCodeResponse extends BaseResponse { + verified!: boolean; +} \ No newline at end of file diff --git a/src/data/response/index.ts b/src/data/response/index.ts index 1473a61..492e48e 100644 --- a/src/data/response/index.ts +++ b/src/data/response/index.ts @@ -2,4 +2,7 @@ export * from './account/CheckDuplicationResponse'; export * from './account/SendVerificationCodeResponse'; export * from './account/VerifyCodeResponse'; export * from './account/SignupResponse'; -export * from './account/LoginResponse'; \ No newline at end of file +export * from './account/LoginResponse'; +export * from './account/SendResetPasswordCodeResponse'; +export * from './account/VerifyResetPasswordCodeResponse'; +export * from './account/ResetPasswordResponse'; \ No newline at end of file diff --git a/src/network/AccountNetwork.ts b/src/network/AccountNetwork.ts index 6fe868e..495e744 100644 --- a/src/network/AccountNetwork.ts +++ b/src/network/AccountNetwork.ts @@ -3,14 +3,20 @@ import { SendVerificationCodeRequest, VerifyCodeRequest, SignupRequest, - LoginRequest + LoginRequest, + SendResetPasswordCodeRequest, + VerifyResetPasswordCodeRequest, + ResetPasswordRequest } from "@/data/request"; import { CheckDuplicationResponse, SendVerificationCodeResponse, VerifyCodeResponse, SignupResponse, - LoginResponse + LoginResponse, + SendResetPasswordCodeResponse, + VerifyResetPasswordCodeResponse, + ResetPasswordResponse } from "@/data/response"; import { BaseNetwork } from "./BaseNetwork"; @@ -67,4 +73,25 @@ export class AccountNetwork extends BaseNetwork { } ); } + + async sendResetPasswordCode(data: SendResetPasswordCodeRequest) { + return await this.post( + '/send-reset-password-code', + data + ); + } + + async verifyResetPasswordCode(data: VerifyResetPasswordCodeRequest) { + return await this.post( + '/verify-reset-password-code', + data + ); + } + + async resetPassword(data: ResetPasswordRequest) { + return await this.post( + '/reset-password', + data + ); + } } \ No newline at end of file diff --git a/src/ui/page/login/LoginPage.tsx b/src/ui/page/account/login/LoginPage.tsx similarity index 100% rename from src/ui/page/login/LoginPage.tsx rename to src/ui/page/account/login/LoginPage.tsx diff --git a/src/ui/page/signup/SignUpPage.tsx b/src/ui/page/account/signup/SignUpPage.tsx similarity index 100% rename from src/ui/page/signup/SignUpPage.tsx rename to src/ui/page/account/signup/SignUpPage.tsx diff --git a/src/ui/page/resetPassword/ResetPasswordPage.tsx b/src/ui/page/resetPassword/ResetPasswordPage.tsx deleted file mode 100644 index a421072..0000000 --- a/src/ui/page/resetPassword/ResetPasswordPage.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Card, CardContent, CardHeader, CardFooter } from '@/components/ui/card'; -import { ResetPasswordSchema } from '@/data/form'; -import { Field, FieldError, FieldLabel } from '@/components/ui/field'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useState, useCallback } from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import { useNavigate } from 'react-router-dom'; -import { PageRouting } from '@/const/PageRouting'; -import * as z from 'zod'; - -export default function ResetPasswordPage() { - const navigate = useNavigate(); - - const loginForm = useForm>({ - resolver: zodResolver(ResetPasswordSchema), - defaultValues: { - email: "" - } - }); - - const moveToLoginPage = useCallback(() => { - navigate(PageRouting["LOGIN"].path); - }, []); - - return ( -
- - - 비밀번호 초기화 - - -
- ( - - 이메일 - - - - )} - > - -
-
- - - - -
-
- ) -} \ No newline at end of file diff --git a/src/util/Validator.ts b/src/util/Validator.ts index f23e45b..6ca4bf6 100644 --- a/src/util/Validator.ts +++ b/src/util/Validator.ts @@ -1,7 +1,22 @@ export class Validator { static isEmail = (value: any) => { - if (typeof value !== 'string') return false; - const email = value.trim(); - return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(email); -}; + if (typeof value !== 'string') return false; + const email = value.trim(); + return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(email); + }; + + static validatePasswordFormat = (password: string): boolean => { + if (password.length < 8) return false; + + const alphabets = 'abcdefghijklmnopqrstuvwxyz'; + const numbers = '0123456789'; + const specials = '!@#$%^'; + + if (!alphabets.includes(password[0])) return false; + + const hasNumber = [...numbers].some((char) => password.includes(char)); + const hasSpecial = [...specials].some((char) => password.includes(char)); + + return hasNumber && hasSpecial; + } } \ No newline at end of file