From 53dea825b004eccec34deb9e70125cb3cf188f3c Mon Sep 17 00:00:00 2001 From: wallace Date: Tue, 25 Nov 2025 11:02:18 +0900 Subject: [PATCH] register form redesign --- src/app/register/RegisterForm.tsx | 169 ++++++++++++++++++----- src/app/svgs/callendar.tsx | 46 +++++++ src/lib/email-templates.ts | 222 ------------------------------ src/lib/prisma.ts | 14 -- 4 files changed, 182 insertions(+), 269 deletions(-) create mode 100644 src/app/svgs/callendar.tsx delete mode 100644 src/lib/email-templates.ts delete mode 100644 src/lib/prisma.ts diff --git a/src/app/register/RegisterForm.tsx b/src/app/register/RegisterForm.tsx index 22ceb86..8d737ac 100644 --- a/src/app/register/RegisterForm.tsx +++ b/src/app/register/RegisterForm.tsx @@ -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(""); 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>({}); const [errors, setErrors] = useState>({}); + // 휴대폰 번호 포맷팅 함수 + 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" /> - - {gender === "MALE" && } + + {gender === "MALE" && } 남성 @@ -463,8 +524,8 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo onChange={() => setGender("FEMALE")} className="sr-only" /> - - {gender === "FEMALE" && } + + {gender === "FEMALE" && } 여성 @@ -475,14 +536,56 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo {/* 생년월일 */}
- 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" - /> +
+ { + 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" + /> + + { + setBirthdate(e.target.value); + setBirthdateInput(""); + }} + className="absolute right-50 top-1 h-full w-[20px] opacity-0 cursor-pointer" + /> +
{errors.birthdate &&

{errors.birthdate}

}
diff --git a/src/app/svgs/callendar.tsx b/src/app/svgs/callendar.tsx new file mode 100644 index 0000000..814cf82 --- /dev/null +++ b/src/app/svgs/callendar.tsx @@ -0,0 +1,46 @@ +import React from "react"; + +const CalendarSvg: React.FC> = (props) => ( + + + + + + + +); + +export default CalendarSvg; diff --git a/src/lib/email-templates.ts b/src/lib/email-templates.ts deleted file mode 100644 index e471349..0000000 --- a/src/lib/email-templates.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * 이메일 템플릿 유틸리티 - * Figma 디자인에 맞춘 이메일 템플릿을 생성합니다. - */ - -/** - * 이메일 인증번호 템플릿 생성 - * @param verificationCode - 인증번호 (6자리) - * @param baseUrl - 이미지 베이스 URL (선택사항, 기본값: '') - * @returns HTML 형식의 이메일 템플릿 - */ -export function generateVerificationEmailTemplate( - verificationCode: string, - baseUrl: string = '' -): string { - // 이미지 URL 생성 (baseUrl이 제공되면 사용, 없으면 상대 경로) - const getImageUrl = (imagePath: string) => { - if (baseUrl) { - return `${baseUrl}${imagePath}`; - } - return imagePath; - }; - - return ` - - - - - - [XR LMS] 이메일 인증 번호 - - - - - - - `.trim(); -} - -/** - * 이메일 인증번호 템플릿 (텍스트 버전) - * @param verificationCode - 인증번호 (6자리) - * @returns 텍스트 형식의 이메일 템플릿 - */ -export function generateVerificationEmailTextTemplate(verificationCode: string): string { - return ` -[XR LMS] 이메일 인증 번호 - -인증번호: ${verificationCode} - -위 번호를 인증번호 입력창에 입력해 주세요. -인증번호 입력 후 해당 화면 내 [인증번호 확인]버튼을 클릭해 주세요. - -Copyright ⓒ 2025 XL LMS. All rights reserved - `.trim(); -} - diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts deleted file mode 100644 index 0eebf59..0000000 --- a/src/lib/prisma.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const globalForPrisma = globalThis as unknown as { - prisma: PrismaClient | undefined; -}; - -export const prisma = - globalForPrisma.prisma ?? - new PrismaClient({ - log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], - }); - -if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; -