From a4ecfd53ddc69cefb4f32b5e91f8458ca155cc07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AF=BC=EA=B1=B4=ED=9D=AC?= Date: Sun, 9 Nov 2025 20:30:46 +0900 Subject: [PATCH] =?UTF-8?q?issue=20#=20ci/cd=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 9 + package.json | 2 +- src/data/form/emailVerification.schema.ts | 9 + src/data/form/index.ts | 3 +- src/data/form/login.schema.ts | 8 +- src/data/form/resetPassword.schema.ts | 4 +- src/data/form/signup.schema.ts | 13 +- src/hooks/useModal.tsx | 40 +++++ src/index.css | 12 ++ .../modal/EmailVerificationModal.tsx | 98 +++++++++++ src/ui/page/login/LoginPage.tsx | 0 src/ui/page/signup/SignUpPage.tsx | 160 +++++++++++++++++- 12 files changed, 333 insertions(+), 25 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 src/data/form/emailVerification.schema.ts create mode 100644 src/hooks/useModal.tsx create mode 100644 src/ui/component/modal/EmailVerificationModal.tsx create mode 100644 src/ui/page/login/LoginPage.tsx diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..e0970ed --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,9 @@ +stages: + -build + +build: + stage: build + image: node:25.1.0 + script: + - npm run build + - ls diff --git a/package.json b/package.json index 4ea66b0..27c6795 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && vite build", + "build": "vite build", "lint": "eslint .", "preview": "vite preview" }, diff --git a/src/data/form/emailVerification.schema.ts b/src/data/form/emailVerification.schema.ts new file mode 100644 index 0000000..29993bb --- /dev/null +++ b/src/data/form/emailVerification.schema.ts @@ -0,0 +1,9 @@ +import * as z from 'zod'; + +export const EmailVerificationSchema = z.object({ + email: z + .email() + , verificationCode: z + .string() + .length(6, "이메일 인증 번호 6자리를 입력해주십시오.") +}); \ No newline at end of file diff --git a/src/data/form/index.ts b/src/data/form/index.ts index 334dd49..02e4965 100644 --- a/src/data/form/index.ts +++ b/src/data/form/index.ts @@ -1,3 +1,4 @@ export { SignUpSchema } from './signup.schema'; export { LoginSchema } from './login.schema'; -export { ResetPasswordSchema } from './resetPassword.schema'; \ No newline at end of file +export { ResetPasswordSchema } from './resetPassword.schema'; +export { EmailVerificationSchema } from './emailVerification.schema'; \ No newline at end of file diff --git a/src/data/form/login.schema.ts b/src/data/form/login.schema.ts index d380697..87c73f3 100644 --- a/src/data/form/login.schema.ts +++ b/src/data/form/login.schema.ts @@ -1,13 +1,11 @@ import * as z from 'zod'; -import { zodResolver } from '@/hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -const LoginSchema = z.object({ +export const LoginSchema = z.object({ email: z .email() , password: z - .string() + .string() .min(8, "비밀번호는 8-12 자리여야 합니다.") .max(12, "비밀번호는 8-12 자리여야 합니다.") - .regex(\^[a-z](?=.*[0-9])(?=.*[!@#$]).*$\) + .regex(/^[a-z](?=.*[0-9])(?=.*[!@#$]).*$/, "비밀번호는 영소문자로 시작하여 숫자, 특수문자(!@#$)를 한 개 이상 포함하여야 합니다.") }); \ No newline at end of file diff --git a/src/data/form/resetPassword.schema.ts b/src/data/form/resetPassword.schema.ts index ad100e5..73360b3 100644 --- a/src/data/form/resetPassword.schema.ts +++ b/src/data/form/resetPassword.schema.ts @@ -1,8 +1,6 @@ import * as z from 'zod'; -import { zodResolver } from '@/hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -const ResetPasswordSchema = z.object({ +export const ResetPasswordSchema = z.object({ email: z .email() }); \ No newline at end of file diff --git a/src/data/form/signup.schema.ts b/src/data/form/signup.schema.ts index 66c1819..b3fb6df 100644 --- a/src/data/form/signup.schema.ts +++ b/src/data/form/signup.schema.ts @@ -1,19 +1,20 @@ import * as z from 'zod'; -import { zodResolver } from '@/hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -const SignUpSchema = z.object({ +export const SignUpSchema = z.object({ email: z - .email() + .string() + .min(1, "이메일을 입력해주십시오.") , password: z - .string() + .string() .min(8, "비밀번호는 8-12 자리여야 합니다.") .max(12, "비밀번호는 8-12 자리여야 합니다.") - .regex(\^[a-z](?=.*[0-9])(?=.*[!@#$]).*$\, "영문 소문자로 시작하고 숫자와 특수문자(!@#$)를 포함해야 합니다.") + .regex(/^[a-z](?=.*[0-9])(?=.*[!@#$]).*$/, "영문 소문자로 시작하고 숫자와 특수문자(!@#$)를 포함해야 합니다.") , name: z .string() + .min(1, "이름을 입력해주시십시오.") , nickname: z .string() + .min(1, "닉네임을 입력해주십시오.") , passwordConfirm: z .string() }) diff --git a/src/hooks/useModal.tsx b/src/hooks/useModal.tsx new file mode 100644 index 0000000..be275c9 --- /dev/null +++ b/src/hooks/useModal.tsx @@ -0,0 +1,40 @@ +import { useState, type ReactNode } from "react"; +import { Dialog, DialogTrigger, DialogTitle, DialogContent, DialogHeader, DialogFooter} from "@/components/ui/dialog"; +import { type DialogProps } from "@radix-ui/react-dialog"; + +export function useModal() { + const [isOpen, setIsOpen] = useState(false); + + const open = () => setIsOpen(true); + const close = () => setIsOpen(false); + + type ModalProps = DialogProps & { + children: ReactNode; + trigger: ReactNode; + title: string; + footer: ReactNode; + }; + + const Modal = ({ children, trigger, title, footer, ...props }: ModalProps) => ( + setIsOpen(val)} + modal={true} + {...props}> + + {trigger} + + + + {title} + + {children} + + {footer} + + + + ) + + return { isOpen, open, close, Modal }; +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index 0e53474..c913384 100644 --- a/src/index.css +++ b/src/index.css @@ -122,3 +122,15 @@ html, body, #root { height: 100%; } + +/* Chrome, Safari, Edge */ +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Firefox */ +input[type="number"] { + -moz-appearance: textfield; +} \ No newline at end of file diff --git a/src/ui/component/modal/EmailVerificationModal.tsx b/src/ui/component/modal/EmailVerificationModal.tsx new file mode 100644 index 0000000..35d3cd0 --- /dev/null +++ b/src/ui/component/modal/EmailVerificationModal.tsx @@ -0,0 +1,98 @@ +import { Button } from "@/components/ui/button"; +import { useModal } from "@/hooks/useModal"; +import { useState, type ReactNode } from "react"; +import { EmailVerificationSchema } from "@/data/form"; +import { Controller, useForm } from "react-hook-form"; +import z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"; +import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"; + +type EmailVerificationModalProps = { + trigger: ReactNode; + email: string; + handler: () => void; +} +export default function EmailVerificationModal( + { trigger, email, handler } : EmailVerificationModalProps +) { + const [isVerifying, setIsVerifying] = useState(false); + const { open, close, Modal } = useModal(); + + const emailVerificationForm = useForm>({ + resolver: zodResolver(EmailVerificationSchema), + defaultValues: { + email: email, + verificationCode: "" + } + }); + + const handleOnOpenChange = (isOpen: boolean) => { + if (!isVerifying) { + if (isOpen) { + open(); + } else { + emailVerificationForm.clearErrors(); + close(); + } + } + } + + const verifyCode = () => { + console.log(emailVerificationForm.getValues("verificationCode")); + } + + const handleOnOTPComplete = (otp: string) => { + setIsVerifying(true); + emailVerificationForm.setValue("verificationCode", otp, { shouldValidate: true }); + emailVerificationForm.handleSubmit(verifyCode)(); + } + + return ( + + ( + + + 인증 번호 + +
+ + + + + + + + + + +
+ +
+ )} + /> +
+ + } + trigger={trigger} + title="이메일 인증" + footer={null} + />); +} \ No newline at end of file diff --git a/src/ui/page/login/LoginPage.tsx b/src/ui/page/login/LoginPage.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/page/signup/SignUpPage.tsx b/src/ui/page/signup/SignUpPage.tsx index 8277097..e05dd5b 100644 --- a/src/ui/page/signup/SignUpPage.tsx +++ b/src/ui/page/signup/SignUpPage.tsx @@ -5,23 +5,165 @@ import { Input } from '@/components/ui/input'; import { Card, CardContent, - CardDescription, CardHeader, - CardTitle + CardFooter } from '@/components/ui/card'; -import { Form, FormField } from '@/components/ui/form'; +import { Field, FieldError, FieldGroup, FieldLabel, FieldLegend } from '@/components/ui/field'; +import { SignUpSchema } from '@/data/form'; +import { Controller, useForm } from 'react-hook-form'; +import * as z from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import EmailVerificationModal from '@/ui/component/modal/EmailVerificationModal'; export default function SignUpPage() { - const [open, setOpen] = useState(false); + const [isCheckedEmailDuplication, setIsCheckdEmailDupliation] = useState(false); + const [isEmailVerificated, setIsEmailVerificated] = useState(false); + const [duplicationCheckedEmail, setDuplicationCheckedEmail] = useState(""); + + const signUpForm = useForm>({ + resolver: zodResolver(SignUpSchema), + defaultValues: { + email: "", + password: "", + passwordConfirm: "", + name: "", + nickname: "", + } + }); + + const handleOnChangeEmail = (e: React.ChangeEvent) => { + setIsCheckdEmailDupliation( + e.currentTarget.value === duplicationCheckedEmail + ); + // setIsEmailVerificated( + // e.currentTarget.value === duplicationCheckedEmail + // ); + } + + const handleEmailDuplicationCheckButtonClick = () => { + console.log(signUpForm.getValues("email")); + setDuplicationCheckedEmail(signUpForm.getValues("email")); + setIsCheckdEmailDupliation(true); + } + + const handleOnSubmitSignUpForm = () => { + if (!isCheckedEmailDuplication) { + signUpForm.setError("email", { message: "이메일 중복 확인이 필요합니다." }); + } + // if (!isEmailVerificated) { + // signUpForm.setError("email", { message: "이메일 인증이 완료되지 않았습니다." }); + // } + } + return (
- - - 로그인 - + + 회원가입 - +
+ + ( + + 이름 + + + + )} + /> + ( + + 닉네임 + + + + )} + /> + ( + + 이메일 +
+ + +
+ +
+ )} + /> + ( + + 비밀번호 + + + + )} + /> + ( + + 비밀번호 확인 + + + + )} + /> +
+
+ + + 회원가입 + + } + email={duplicationCheckedEmail} + handler={() => {}} + /> + +
);