register form redesign

This commit is contained in:
wallace
2025-11-25 11:02:18 +09:00
parent 71feecb8d1
commit 53dea825b0
4 changed files with 182 additions and 269 deletions

View File

@@ -5,6 +5,7 @@ import Link from "next/link";
import LoginCheckboxActiveSvg from "@/app/svgs/logincheckboxactivesvg";
import LoginCheckboxInactiveSvg from "@/app/svgs/logincheckboxinactivesvg";
import LoginInputSvg from "@/app/svgs/inputformx";
import CalendarSvg from "@/app/svgs/callendar";
type Gender = "MALE" | "FEMALE" | "";
@@ -21,6 +22,7 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
const [passwordConfirm, setPasswordConfirm] = useState("");
const [gender, setGender] = useState<Gender>("");
const [birthdate, setBirthdate] = useState("");
const [birthdateInput, setBirthdateInput] = useState("");
// 이메일 인증 관련
const [emailCodeSent, setEmailCodeSent] = useState(false);
@@ -41,9 +43,57 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
const [focused, setFocused] = useState<Record<string, boolean>>({});
const [errors, setErrors] = useState<Record<string, string>>({});
// 휴대폰 번호 포맷팅 함수
function formatPhoneNumber(phoneNumber: string): string {
const numbers = phoneNumber.replace(/[^0-9]/g, "");
if (numbers.length <= 3) return numbers;
if (numbers.length <= 7) return `${numbers.slice(0, 3)}-${numbers.slice(3)}`;
if (numbers.length <= 11) return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`;
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7, 11)}`;
}
// 생년월일 YYYYMMDD를 YYYY-MM-DD로 변환하는 함수
function parseBirthdate(input: string): string {
// 숫자만 추출
const numbers = input.replace(/[^0-9]/g, "");
// YYYYMMDD 형식인지 확인 (8자리)
if (numbers.length === 8) {
const year = numbers.slice(0, 4);
const month = numbers.slice(4, 6);
const day = numbers.slice(6, 8);
// 유효한 날짜인지 검증
const yearNum = parseInt(year, 10);
const monthNum = parseInt(month, 10);
const dayNum = parseInt(day, 10);
if (yearNum >= 1900 && yearNum <= 2100 && monthNum >= 1 && monthNum <= 12 && dayNum >= 1 && dayNum <= 31) {
// 날짜 유효성 검사
const date = new Date(yearNum, monthNum - 1, dayNum);
if (date.getFullYear() === yearNum && date.getMonth() === monthNum - 1 && date.getDate() === dayNum) {
return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
}
}
}
// YYYY-MM-DD 형식이면 그대로 반환
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
return input;
}
return "";
}
const isEmailValid = useMemo(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email), [email]);
const isPhoneValid = useMemo(() => /^\d{9,11}$/.test(phone), [phone]);
const isPasswordValid = useMemo(() => password.length >= 8 && password.length <= 16, [password]);
const isPasswordValid = useMemo(() => {
if (password.length < 8 || password.length > 16) return false;
const hasEnglish = /[a-zA-Z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
return hasEnglish && hasNumber && hasSpecialChar;
}, [password]);
const isPasswordConfirmValid = useMemo(() => passwordConfirm === password && passwordConfirm.length > 0, [password, passwordConfirm]);
const canSubmit = useMemo(() => {
@@ -64,7 +114,18 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
if (name.trim().length === 0) nextErrors.name = "이름을 입력해 주세요.";
if (!isPhoneValid) nextErrors.phone = "휴대폰 번호를 숫자만 9~11자리로 입력해 주세요.";
if (!isEmailValid) nextErrors.email = "올바른 이메일 형식을 입력해 주세요.";
if (!isPasswordValid) nextErrors.password = "비밀번호는 8~16자여야 합니다.";
if (!isPasswordValid) {
if (password.length < 8 || password.length > 16) {
nextErrors.password = "비밀번호는 8~16자여야 합니다.";
} else {
const hasEnglish = /[a-zA-Z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
if (!hasEnglish || !hasNumber || !hasSpecialChar) {
nextErrors.password = "8~16자의 영문/숫자/특수문자를 조합해서 입력해 주세요.";
}
}
}
if (!isPasswordConfirmValid) nextErrors.passwordConfirm = "비밀번호가 일치하지 않습니다.";
if (gender === "") nextErrors.gender = "성별을 선택해 주세요.";
if (birthdate.trim().length === 0) nextErrors.birthdate = "생년월일을 선택해 주세요.";
@@ -97,13 +158,13 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
}
async function verifyEmailCode() {
try{
try {
const response = await fetch(
"https://hrdi.coconutmeet.net/auth/verify-email/confirm",
{
method: "POST", headers: {"Content-Type": "application/json",},
body: JSON.stringify({email: email,emailCode: emailCode})
});
method: "POST", headers: { "Content-Type": "application/json", },
body: JSON.stringify({ email: email, emailCode: emailCode })
});
if (!response.ok) {
console.error("이메일 인증번호 검증 실패:", response.statusText);
onOpenCodeError();
@@ -112,7 +173,7 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
// 인증 성공 시 상태 업데이트
setEmailCodeVerified(true);
}
catch(error){
catch (error) {
console.error("이메일 인증번호 검증 오류:", error);
onOpenCodeError();
}
@@ -120,16 +181,16 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
async function sendEmailCode() {
if (!isEmailValid) return;
// INSERT_YOUR_CODE
try {
const response = await fetch(
"https://hrdi.coconutmeet.net/auth/verify-email/send",
{
{
method: "POST",
headers: {"Content-Type": "application/json",},
body: JSON.stringify({email: email})
headers: { "Content-Type": "application/json", },
body: JSON.stringify({ email: email })
}
);
if (!response.ok) {
@@ -166,15 +227,15 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
try {
const response = await fetch("https://hrdi.coconutmeet.net/auth/signup", {
method: "POST",
headers: {"Content-Type": "application/json",},
headers: { "Content-Type": "application/json", },
body: JSON.stringify({
email: email,
emailCode: emailCode,
password: password,
passwordConfirm: passwordConfirm,
name: name,
phone: phone,
gender: gender,
email: email,
emailCode: emailCode,
password: password,
passwordConfirm: passwordConfirm,
name: name,
phone: phone,
gender: gender,
birthDate: birthdate
})
});
@@ -254,11 +315,11 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
name="phone"
type="tel"
inputMode="numeric"
value={phone}
value={formatPhoneNumber(phone)}
placeholder="-없이 입력해 주세요."
onChange={(e) => setPhone(e.target.value.replace(/[^0-9]/g, ""))}
onFocus={() => setFocused((p) => ({ ...p, phone: true }))}
onBlur={() => setFocused((p) => ({ ...p, phone: false }))}
placeholder="-없이 입력해 주세요."
className="h-[40px] px-[12px] py-[7px] w-full rounded-[8px] border border-neutral-40 focus:outline-none focus:border-neutral-700 text-[18px] text-neutral-700 placeholder:text-input-placeholder-text pr-[40px]"
/>
{phone.trim().length > 0 && focused.phone && (
@@ -449,8 +510,8 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
onChange={() => setGender("MALE")}
className="sr-only"
/>
<span className={`flex items-center justify-center rounded-full size-[18px] border ${gender === "MALE" ? "border-active-button" : "border-[#8c95a1]"}`}>
{gender === "MALE" && <span className="block size-[9px] rounded-full bg-active-button" />}
<span className={`relative inline-flex items-center justify-center rounded-full w-[18px] h-[18px] shrink-0 border box-border ${gender === "MALE" ? "border-active-button" : "border-[#8c95a1]"}`}>
{gender === "MALE" && <span className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 block size-[9px] rounded-full bg-active-button" />}
</span>
</label>
@@ -463,8 +524,8 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
onChange={() => setGender("FEMALE")}
className="sr-only"
/>
<span className={`flex items-center justify-center rounded-full size-[18px] border ${gender === "FEMALE" ? "border-active-button" : "border-[#8c95a1]"}`}>
{gender === "FEMALE" && <span className="block size-[9px] rounded-full bg-active-button" />}
<span className={`relative inline-flex items-center justify-center rounded-full w-[18px] h-[18px] shrink-0 border box-border ${gender === "FEMALE" ? "border-active-button" : "border-[#8c95a1]"}`}>
{gender === "FEMALE" && <span className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 block size-[9px] rounded-full bg-active-button" />}
</span>
</label>
@@ -475,14 +536,56 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
{/* 생년월일 */}
<div className="space-y-2">
<label htmlFor="birthdate" className="text-[15px] font-semibold text-[#6c7682]"></label>
<input
id="birthdate"
name="birthdate"
type="date"
value={birthdate}
onChange={(e) => setBirthdate(e.target.value)}
className="h-[40px] px-[12px] py-[7px] w-full rounded-[8px] border border-neutral-40 focus:outline-none focus:border-neutral-700 text-[16px] text-neutral-700"
/>
<div className="relative">
<input
id="birthdate"
name="birthdate"
type="text"
value={birthdateInput || (birthdate ? birthdate.replace(/-/g, ".") : "")}
onChange={(e) => {
const inputValue = e.target.value;
setBirthdateInput(inputValue);
if (inputValue === "") {
setBirthdate("");
}
}}
onBlur={(e) => {
// 포커스를 잃을 때 YYYYMMDD 형식이면 변환
const formatted = parseBirthdate(e.target.value);
if (formatted) {
setBirthdate(formatted);
setBirthdateInput("");
} else if (e.target.value === "") {
setBirthdate("");
setBirthdateInput("");
}
}}
placeholder="생년월일"
className="h-[40px] px-[12px] w-full rounded-[8px] border border-neutral-40 focus:outline-none focus:border-neutral-700 text-[18px] text-neutral-700 placeholder:text-input-placeholder-text pr-[40px] flex items-center"
/>
<button
type="button"
onClick={(e) => {
e.preventDefault();
const dateInput = document.getElementById("birthdate-date-picker") as HTMLInputElement;
dateInput?.showPicker?.();
}}
className="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer"
aria-label="날짜 선택"
>
<CalendarSvg width="20" height="20" />
</button>
<input
id="birthdate-date-picker"
type="date"
value={birthdate}
onChange={(e) => {
setBirthdate(e.target.value);
setBirthdateInput("");
}}
className="absolute right-50 top-1 h-full w-[20px] opacity-0 cursor-pointer"
/>
</div>
{errors.birthdate && <p className="text-error text-[13px] leading-tight">{errors.birthdate}</p>}
</div>

View File

@@ -0,0 +1,46 @@
import React from "react";
const CalendarSvg: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M16 2V6"
stroke="#333C47"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8 2V6"
stroke="#333C47"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3 9H21"
stroke="#333C47"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19 4H5C3.895 4 3 4.895 3 6V19C3 20.105 3.895 21 5 21H19C20.105 21 21 20.105 21 19V6C21 4.895 20.105 4 19 4Z"
stroke="#333C47"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<rect x="3" y="4" width="18" height="5" rx="2" fill="#333C47" />
</svg>
);
export default CalendarSvg;