-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/email register #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
389a056
feat: 이메일 회원가입
dasosann 7554fff
Merge branch 'main' into feat/email-register
dasosann 0fa65ae
feat: 배경 zIndex isolate로 조절
dasosann 677fbb5
feat: 이메일 회원가입 퍼블리싱
dasosann 15a60f8
feat: 회원가입 api 연결
dasosann d7aca1f
feat: 코드리뷰 반영
dasosann c0e3d69
feat: 코드리뷰 반영 2
dasosann File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import Button from "@/components/ui/Button"; | ||
| import FormInput from "@/components/ui/FormInput"; | ||
| import { validateEmail } from "@/lib/validators"; | ||
| import React, { useState } from "react"; | ||
|
|
||
| type EmailStepProps = { | ||
| email: string; | ||
| onEmailChange: (email: string) => void; | ||
| onSubmit: (e: React.SyntheticEvent<HTMLFormElement>) => void; | ||
| }; | ||
|
|
||
| export const EmailStep = ({ | ||
| email, | ||
| onEmailChange, | ||
| onSubmit, | ||
| }: EmailStepProps) => { | ||
| const [emailError, setEmailError] = useState(false); | ||
|
|
||
| const handleEmailChange = (value: string) => { | ||
| onEmailChange(value); | ||
| if (emailError && value) { | ||
| setEmailError(!validateEmail(value)); | ||
| } | ||
| }; | ||
|
|
||
| const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => { | ||
| e.preventDefault(); | ||
|
|
||
| if (!validateEmail(email)) { | ||
| setEmailError(true); | ||
| return; | ||
| } | ||
|
|
||
| setEmailError(false); | ||
| onSubmit(e); | ||
| }; | ||
|
|
||
| return ( | ||
| <section className="mt-10 flex w-full flex-1 flex-col items-start gap-8"> | ||
| <div className="flex flex-col"> | ||
| <span className="typo-13-600 text-color-flame-700"> | ||
| STEP 1 <span className="text-gray-300">/ 2</span> | ||
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </span> | ||
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <h1 className="typo-28-600 mt-4 mb-1 text-black"> | ||
| 이메일을 입력해주세요 | ||
| </h1> | ||
| <h2 className="typo-16-500 text-gray-600"> | ||
| 이메일은 로그인 시 아이디로 사용됩니다. | ||
| </h2> | ||
| </div> | ||
|
|
||
| <form | ||
| className="flex w-full flex-1 flex-col gap-4" | ||
| onSubmit={handleSubmit} | ||
| > | ||
| <div className="flex w-full flex-col gap-2"> | ||
| <label htmlFor="email" className="sr-only"> | ||
| 이메일 | ||
| </label> | ||
| <FormInput | ||
| id="email" | ||
| type="email" | ||
| name="email" | ||
| placeholder="이메일 입력" | ||
| autoComplete="email" | ||
| value={email} | ||
| onChange={(e) => handleEmailChange(e.target.value)} | ||
| error={emailError} | ||
| /> | ||
| {emailError && ( | ||
| <span className="typo-12-400 text-color-text-highlight"> | ||
| *이메일 형식이 올바르지 않습니다. | ||
| </span> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className="mt-auto"> | ||
| <Button shadow={true} type="submit"> | ||
| 다음 | ||
| </Button> | ||
| </div> | ||
| </form> | ||
| </section> | ||
| ); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import Button from "@/components/ui/Button"; | ||
| import FormInput from "@/components/ui/FormInput"; | ||
| import { | ||
| validatePasswordLength, | ||
| validatePasswordPattern, | ||
| } from "@/lib/validators"; | ||
| import { Check, Eye, EyeOff, X } from "lucide-react"; | ||
| import React, { useState } from "react"; | ||
|
|
||
| type PasswordStepProps = { | ||
| password: string; | ||
| onPasswordChange: (password: string) => void; | ||
| onSubmit: (e: React.SyntheticEvent<HTMLFormElement>) => void; | ||
| }; | ||
|
|
||
| export const PasswordStep = ({ | ||
| password, | ||
| onPasswordChange, | ||
| onSubmit, | ||
| }: PasswordStepProps) => { | ||
| const [showPassword, setShowPassword] = useState(false); | ||
|
|
||
| const isLengthValid = validatePasswordLength(password); | ||
| const isPatternValid = validatePasswordPattern(password); | ||
|
|
||
| return ( | ||
| <section className="mt-10 flex w-full flex-1 flex-col items-start gap-8"> | ||
| <div className="flex flex-col"> | ||
| <span className="typo-13-600 text-color-flame-700"> | ||
| STEP 2 <span className="text-gray-300">/ 2</span> | ||
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </span> | ||
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <h1 className="typo-28-600 mt-4 mb-1 text-black"> | ||
| 비밀번호를 설정해주세요 | ||
| </h1> | ||
| <h2 className="typo-16-500 text-gray-600"> | ||
| 안전한 비밀번호로 계정을 보호하세요 | ||
| </h2> | ||
| </div> | ||
|
|
||
| <form className="flex w-full flex-1 flex-col gap-4" onSubmit={onSubmit}> | ||
| <div className="relative flex w-full items-center"> | ||
| <label htmlFor="password" className="sr-only"> | ||
| 비밀번호 | ||
| </label> | ||
| <FormInput | ||
| id="password" | ||
| type={showPassword ? "text" : "password"} | ||
| name="password" | ||
| placeholder="8자 이상 비밀번호 입력" | ||
| autoComplete="new-password" | ||
| value={password} | ||
| onChange={(e) => onPasswordChange(e.target.value)} | ||
| className={password ? "pr-20" : "pr-10"} | ||
| /> | ||
| <div className="absolute right-2 flex items-center gap-[27px]"> | ||
| {password && ( | ||
| <button | ||
| type="button" | ||
| aria-label={showPassword ? "비밀번호 숨기기" : "비밀번호 보기"} | ||
| onClick={() => setShowPassword((prev) => !prev)} | ||
| className="flex items-center text-gray-400 hover:text-gray-600" | ||
| > | ||
| {showPassword ? ( | ||
| <EyeOff width={18} height={18} strokeWidth={2} /> | ||
| ) : ( | ||
| <Eye width={18} height={18} strokeWidth={2} /> | ||
| )} | ||
| </button> | ||
| )} | ||
| {password && ( | ||
| <button | ||
| type="button" | ||
| aria-label="입력 초기화" | ||
| onClick={() => onPasswordChange("")} | ||
| className="flex h-[20px] w-[20px] items-center justify-center rounded-full border border-(--inputfield-close-stroke) bg-(--inputfield-close-fill) text-gray-400" | ||
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| > | ||
| <X width={12} height={12} strokeWidth={2.5} /> | ||
| </button> | ||
| )} | ||
| </div> | ||
| </div> | ||
| <div className="typo-14-400 flex gap-[13px]"> | ||
| <span | ||
| className={`flex items-center gap-1 ${!password || isLengthValid ? "text-color-text-caption2" : "text-color-text-highlight"}`} | ||
| > | ||
| <Check width={14} height={14} strokeWidth={2.5} /> | ||
| 8~20자 이내 | ||
| </span> | ||
| <span | ||
| className={`flex items-center gap-1 ${!password || isPatternValid ? "text-color-text-caption2" : "text-color-text-highlight"}`} | ||
| > | ||
| <Check width={14} height={14} strokeWidth={2.5} /> | ||
| 영문 대소문자, 숫자 포함 | ||
| </span> | ||
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </div> | ||
|
|
||
| <div className="mt-auto"> | ||
| <Button shadow={true} type="submit"> | ||
| 완료 | ||
| </Button> | ||
| </div> | ||
| </form> | ||
| </section> | ||
| ); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| "use client"; | ||
| import { BackButton } from "@/components/ui/BackButton"; | ||
| import { useSendEmail } from "@/hooks/useSendEmail"; | ||
| import { useSignUp } from "@/hooks/useSignUp"; | ||
| import { useVerifyEmail } from "@/hooks/useVerifyEmail"; | ||
| import React, { useState } from "react"; | ||
| import { EmailStep } from "./EmailStep"; | ||
| import { VerificationStep } from "./VerificationStep"; | ||
| import { PasswordStep } from "./PasswordStep"; | ||
|
|
||
| export const ScreenRegister = () => { | ||
| const [step, setStep] = useState<1 | 2 | 3>(1); | ||
| const [email, setEmail] = useState(""); | ||
| const [verificationCode, setVerificationCode] = useState(""); | ||
| const [password, setPassword] = useState(""); | ||
|
|
||
| const { mutate: sendEmail, isPending: isSendingEmail } = useSendEmail(); | ||
| const { mutate: verifyEmail, isPending: isVerifyingEmail } = useVerifyEmail(); | ||
| const { mutate: signUp, isPending: isSigningUp } = useSignUp(); | ||
|
|
||
| const handleEmailSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => { | ||
| e.preventDefault(); | ||
| sendEmail(email, { | ||
| onSuccess: () => setStep(2), | ||
| }); | ||
| }; | ||
|
|
||
| const handleVerify = (code: string, onError: () => void) => { | ||
| verifyEmail( | ||
| { email, code }, | ||
| { | ||
| onSuccess: (data) => { | ||
| if (data.status === 200) { | ||
| setStep(3); | ||
| } else { | ||
| onError(); | ||
| } | ||
| }, | ||
| onError, | ||
| }, | ||
| ); | ||
| }; | ||
|
|
||
| const handlePasswordSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => { | ||
| e.preventDefault(); | ||
| signUp( | ||
| { email, password }, | ||
| { | ||
| onSuccess: (data) => { | ||
| if (data.status === 200) { | ||
| // TODO: 회원가입 완료 후 이동 (예: router.push("/login")) | ||
| } else { | ||
| alert("회원가입에 실패했습니다. 다시 시도해주세요."); | ||
| } | ||
| }, | ||
| onError: () => alert("회원가입에 실패했습니다. 다시 시도해주세요."), | ||
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
dasosann marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, | ||
| ); | ||
| }; | ||
|
|
||
| const handleResendCode = () => { | ||
| sendEmail(email, { | ||
| onError: () => alert("재전송에 실패했습니다. 다시 시도해주세요."), | ||
| }); | ||
| }; | ||
|
|
||
| return ( | ||
| <main className="flex min-h-dvh flex-col items-start px-4 pt-2 pb-[6.2vh]"> | ||
| <BackButton text="회원가입" /> | ||
| {step === 1 && ( | ||
| <EmailStep | ||
| email={email} | ||
| onEmailChange={setEmail} | ||
| onSubmit={handleEmailSubmit} | ||
| /> | ||
| )} | ||
| {step === 2 && ( | ||
| <VerificationStep | ||
| email={email} | ||
| verificationCode={verificationCode} | ||
| onVerificationCodeChange={setVerificationCode} | ||
| onVerify={handleVerify} | ||
| onResend={handleResendCode} | ||
| isVerifying={isVerifyingEmail} | ||
| /> | ||
| )} | ||
| {step === 3 && ( | ||
| <PasswordStep | ||
| password={password} | ||
| onPasswordChange={setPassword} | ||
| onSubmit={handlePasswordSubmit} | ||
| /> | ||
| )} | ||
| </main> | ||
| ); | ||
| }; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
File Walkthrough
이메일 전송 API 연동 커스텀 훅이메일 인증번호 검증 API 연동 훅회원가입 API 연동 커스텀 훅이메일 및 비밀번호 유효성 검사 함수회원가입 페이지 진입점회원가입 3단계 플로우 관리 컴포넌트이메일 입력 단계 UI 컴포넌트인증번호 입력 및 타이머 기능 컴포넌트비밀번호 설정 및 유효성 표시 컴포넌트메타데이터 추가 및 QueryProvider 활성화텍스트 표시 기능 추가 및 레이아웃 개선에러 상태 스타일링 기능 추가색상, 타이포그래피, 입력필드 토큰 확장토큰 생성 스크립트 타이포그래피 범위 확대배경 blur 요소 z-index 조정shadow 스타일 적용 로직 수정