Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions app/layout.tsx

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File Walkthrough

useSendEmail.ts
이메일 전송 API 연동 커스텀 훅                                                                           
useVerifyEmail.ts
이메일 인증번호 검증 API 연동 훅                                                                         
useSignUp.ts
회원가입 API 연동 커스텀 훅                                                                               
validators.ts
이메일 및 비밀번호 유효성 검사 함수                                                                         
page.tsx
회원가입 페이지 진입점                                                                                         
ScreenRegister.tsx
회원가입 3단계 플로우 관리 컴포넌트                                                                         
EmailStep.tsx
이메일 입력 단계 UI 컴포넌트                                                                               
VerificationStep.tsx
인증번호 입력 및 타이머 기능 컴포넌트                                                                       
PasswordStep.tsx
비밀번호 설정 및 유효성 표시 컴포넌트                                                                       
layout.tsx
메타데이터 추가 및 QueryProvider 활성화                                                         
BackButton.tsx
텍스트 표시 기능 추가 및 레이아웃 개선                                                                     
FormInput.tsx
에러 상태 스타일링 기능 추가                                                                                 
tokens.css
색상, 타이포그래피, 입력필드 토큰 확장                                                                     
generate-tokens.js
토큰 생성 스크립트 타이포그래피 범위 확대                                                                   
Blur.tsx
배경 blur 요소 z-index 조정                                                                       
Button.tsx
shadow 스타일 적용 로직 수정                                                                           

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import "./globals.css";
import Blur from "@/components/common/Blur";
import { QueryProvider } from "@/providers/query-provider";
import { ServiceStatusProvider } from "@/providers/service-status-provider";

Check warning on line 6 in app/layout.tsx

View workflow job for this annotation

GitHub Actions / lint

'ServiceStatusProvider' is defined but never used
import { getInitialMaintenanceStatus } from "@/lib/status";

const pretendard = localFont({
Expand All @@ -25,6 +25,20 @@
description: "대학교 축제에서 운명의 인연을 만나보세요!",
type: "website",
locale: "ko_KR",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "코매칭 - 대학축제 커플매칭",
},
],
},
twitter: {
card: "summary_large_image",
title: "코매칭 - 대학축제 커플매칭",
description: "대학교 축제에서 운명의 인연을 만나보세요!",
images: ["/og-image.png"],
},
};

Expand All @@ -40,23 +54,23 @@
}: Readonly<{
children: React.ReactNode;
}>) {
const initialMaintenanceMode = await getInitialMaintenanceStatus();

Check warning on line 57 in app/layout.tsx

View workflow job for this annotation

GitHub Actions / lint

'initialMaintenanceMode' is assigned a value but never used

return (
<html lang="ko" className={pretendard.variable}>
<body
className={`${pretendard.className} flex justify-center bg-white antialiased`}
>
{/* <QueryProvider> */}
{/* <ServiceStatusProvider */}
{/* initialMaintenanceMode={initialMaintenanceMode} */}
{/* > */}
<div className="bg-background-app-base relative min-h-dvh w-full overflow-x-hidden text-black md:max-w-[430px] md:shadow-lg">
<Blur />
{children}
</div>
{/* </ServiceStatusProvider> */}
{/* </QueryProvider> */}
<QueryProvider>
{/* <ServiceStatusProvider */}
{/* initialMaintenanceMode={initialMaintenanceMode} */}
{/* > */}
<div className="bg-background-app-base relative isolate min-h-dvh w-full overflow-x-hidden text-black md:max-w-[430px] md:shadow-lg">
<Blur />
{children}
</div>
{/* </ServiceStatusProvider> */}
</QueryProvider>
</body>
</html>
);
Expand Down
85 changes: 85 additions & 0 deletions app/register/_components/EmailStep.tsx
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>
</span>
<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>
);
};
105 changes: 105 additions & 0 deletions app/register/_components/PasswordStep.tsx
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>
</span>
<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"
>
<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>
</div>

<div className="mt-auto">
<Button shadow={true} type="submit">
완료
</Button>
</div>
</form>
</section>
);
};
96 changes: 96 additions & 0 deletions app/register/_components/ScreenRegister.tsx
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();

Check warning on line 17 in app/register/_components/ScreenRegister.tsx

View workflow job for this annotation

GitHub Actions / lint

'isSendingEmail' is assigned a value but never used
const { mutate: verifyEmail, isPending: isVerifyingEmail } = useVerifyEmail();
const { mutate: signUp, isPending: isSigningUp } = useSignUp();

Check warning on line 19 in app/register/_components/ScreenRegister.tsx

View workflow job for this annotation

GitHub Actions / lint

'isSigningUp' is assigned a value but never used

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("회원가입에 실패했습니다. 다시 시도해주세요."),
},
);
};

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>
);
};
Loading
Loading