로그인 페이지 완성
This commit is contained in:
46
src/app/login/LoginErrorModal.tsx
Normal file
46
src/app/login/LoginErrorModal.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
type LoginErrorModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function LoginErrorModal({ open, onClose }: LoginErrorModalProps) {
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="닫기"
|
||||
className="absolute inset-0 bg-black/40 cursor-default"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="login-error-title"
|
||||
className="relative bg-white box-border flex flex-col items-stretch justify-start p-6 rounded-[8px] min-w-[500px] max-w-[calc(100%-48px)]"
|
||||
>
|
||||
<div className="text-[18px] leading-normal font-semibold text-neutral-700 mb-8" id="login-error-title">
|
||||
아이디 또는 비밀번호가 일치하지 않습니다.
|
||||
<br />
|
||||
확인 후 다시 시도해 주세요.
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-[8px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="h-[40px] min-w-20 px-[12px] rounded-[8px] bg-active-button text-white text-[16px] font-semibold cursor-pointer"
|
||||
>
|
||||
확인
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
53
src/app/login/loginoption.tsx
Normal file
53
src/app/login/loginoption.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
type LoginOptionProps = {
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
loginErrorModalEnabled?: boolean;
|
||||
setLoginErrorModalEnabled?: (enabled: boolean) => void;
|
||||
};
|
||||
|
||||
export default function LoginOption({
|
||||
className,
|
||||
loginErrorModalEnabled,
|
||||
setLoginErrorModalEnabled,
|
||||
}: LoginOptionProps) {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`fixed bottom-2 right-2 bg-red-400 cursor-pointer rounded-full w-[40px] h-[40px] shadow-xl z-100`}
|
||||
>
|
||||
</button>
|
||||
{ isOpen && (
|
||||
<div className="fixed inset-0 flex items-center justify-center z-50">
|
||||
<div className="w-[500px] h-[600px] flex bg-white/80 p-10 border rounded-lg">
|
||||
<ul className="flex flex-col gap-4">
|
||||
<li className="flex items-center justify-between">
|
||||
<p className="mr-4">login error modal</p>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="login error modal 토글"
|
||||
aria-pressed={!!loginErrorModalEnabled}
|
||||
onClick={() => setLoginErrorModalEnabled?.(!loginErrorModalEnabled)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${loginErrorModalEnabled ? 'bg-blue-600' : 'bg-gray-300'}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-white transition ${loginErrorModalEnabled ? 'translate-x-5' : 'translate-x-1'}`}
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,25 +3,48 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import MainLogo from "@/app/svgs/mainlogosvg"
|
||||
import LoginCheckboxActiveSvg from "@/app/svgs/logincheckboxactivesvg";
|
||||
import LoginCheckboxInactiveSvg from "@/app/svgs/logincheckboxinactivesvg";
|
||||
import LoginInputSvg from "@/app/svgs/inputformx";
|
||||
import LoginErrorModal from "./LoginErrorModal";
|
||||
import LoginOption from "@/app/login/loginoption";
|
||||
|
||||
export default function LoginPage() {
|
||||
const [userId, setUserId] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [rememberId, setRememberId] = useState(false);
|
||||
const [autoLogin, setAutoLogin] = useState(false);
|
||||
const [isUserIdFocused, setIsUserIdFocused] = useState(false);
|
||||
const [isPasswordFocused, setIsPasswordFocused] = useState(false);
|
||||
const [isLoginErrorOpen, setIsLoginErrorOpen] = useState(false);
|
||||
const [idError, setIdError] = useState("");
|
||||
const [passwordError, setPasswordError] = useState("");
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
/* todo */
|
||||
console.log({ userId, password, rememberId, autoLogin });
|
||||
// 실제 로그인 API 연동 전까지는 실패 모달을 노출합니다.
|
||||
// API 연동 시 결과에 따라 성공/실패 분기에서 setIsLoginErrorOpen(true) 호출로 교체하세요.
|
||||
// if (userId.trim().length > 0 && password.trim().length > 0) {
|
||||
// setIsLoginErrorOpen(true);
|
||||
// }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full flex flex-col items-center justify-between">
|
||||
<div></div>
|
||||
|
||||
<LoginErrorModal
|
||||
open={isLoginErrorOpen}
|
||||
onClose={() => setIsLoginErrorOpen(false)}
|
||||
/>
|
||||
<LoginOption
|
||||
onClick={() => setIsLoginErrorOpen(true)}
|
||||
loginErrorModalEnabled={isLoginErrorOpen}
|
||||
setLoginErrorModalEnabled={setIsLoginErrorOpen}
|
||||
/>
|
||||
|
||||
<div className="rounded-xl bg-white max-w-[560px] px-[40px] w-full">
|
||||
{/* 로고 영역 */}
|
||||
<div className="my-10 flex flex-col items-center">
|
||||
<div className="my-15 flex flex-col items-center">
|
||||
<div className="mb-[7px]">
|
||||
<MainLogo/>
|
||||
</div>
|
||||
@@ -31,10 +54,10 @@ export default function LoginPage() {
|
||||
</div>
|
||||
|
||||
{/* 폼 */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div className="space-y-4">
|
||||
{/* 아이디 */}
|
||||
<div>
|
||||
<div className="relative">
|
||||
<label htmlFor="userId" className="sr-only">
|
||||
아이디
|
||||
</label>
|
||||
@@ -43,17 +66,33 @@ export default function LoginPage() {
|
||||
name="userId"
|
||||
value={userId}
|
||||
onChange={(e) => setUserId(e.target.value)}
|
||||
onFocus={() => setIsUserIdFocused(true)}
|
||||
onBlur={() => setIsUserIdFocused(false)}
|
||||
placeholder="아이디 (이메일)"
|
||||
className="
|
||||
h-[56px] px-[12px] py-[7px] w-full rounded-[8px] border border-neutral-40
|
||||
focus:outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none
|
||||
focus:appearance-none focus:border-neutral-700
|
||||
text-[18px] text-neutral-700 font-normal leading-[150%] placeholder:text-input-placeholder-text
|
||||
pr-[40px]
|
||||
"
|
||||
/>
|
||||
{userId.trim().length > 0 && isUserIdFocused && (
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
setUserId("");
|
||||
}}
|
||||
aria-label="입력 지우기"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer"
|
||||
>
|
||||
<LoginInputSvg />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* 비밀번호 */}
|
||||
<div>
|
||||
<div className="relative">
|
||||
<label htmlFor="password" className="sr-only">
|
||||
비밀번호
|
||||
</label>
|
||||
@@ -63,27 +102,47 @@ export default function LoginPage() {
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onFocus={() => setIsPasswordFocused(true)}
|
||||
onBlur={() => setIsPasswordFocused(false)}
|
||||
placeholder="비밀번호"
|
||||
className="
|
||||
h-[56px] px-[12px] py-[7px] rounded-[8px] w-full border border-neutral-40
|
||||
focus:outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none
|
||||
focus:appearance-none focus:border-neutral-700
|
||||
text-[18px] text-neutral-700 font-normal leading-[150%] placeholder:text-input-placeholder-text
|
||||
pr-[40px]
|
||||
"
|
||||
/>
|
||||
{password.trim().length > 0 && isPasswordFocused && (
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
setPassword("");
|
||||
}}
|
||||
aria-label="입력 지우기"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer"
|
||||
>
|
||||
<LoginInputSvg />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 체크박스들 */}
|
||||
<div className="flex items-center justify-start gap-6">
|
||||
<div className="flex items-center justify-start gap-6 mb-15">
|
||||
<label className="flex cursor-pointer select-none items-center gap-2 text-[15px] font-normal text-basic-text">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rememberId}
|
||||
onChange={(e) => setRememberId(e.target.checked)}
|
||||
className="h-[18px] w-[18px] cursor-pointer rounded border
|
||||
accent-input-border-select border-inactive-checkbox"
|
||||
className="sr-only"
|
||||
/>
|
||||
{rememberId ? (
|
||||
<LoginCheckboxActiveSvg />
|
||||
) : (
|
||||
<LoginCheckboxInactiveSvg />
|
||||
)}
|
||||
아이디 기억하기
|
||||
</label>
|
||||
<label className="flex cursor-pointer select-none items-center gap-2 text-[15px] font-normal text-basic-text">
|
||||
@@ -91,9 +150,13 @@ export default function LoginPage() {
|
||||
type="checkbox"
|
||||
checked={autoLogin}
|
||||
onChange={(e) => setAutoLogin(e.target.checked)}
|
||||
className="h-[18px] w-[18px] cursor-pointer rounded border
|
||||
accent-input-border-select border-inactive-checkbox"
|
||||
className="sr-only"
|
||||
/>
|
||||
{autoLogin ? (
|
||||
<LoginCheckboxActiveSvg />
|
||||
) : (
|
||||
<LoginCheckboxInactiveSvg />
|
||||
)}
|
||||
자동 로그인
|
||||
</label>
|
||||
</div>
|
||||
@@ -101,16 +164,16 @@ export default function LoginPage() {
|
||||
{/* 로그인 버튼 */}
|
||||
<button
|
||||
type="submit"
|
||||
className="h-14 w-full rounded-lg text-[16px] font-semibold text-white transition-opacity cursor-pointer bg-inactive-button"
|
||||
className={`h-14 w-full rounded-lg text-[16px] font-semibold text-white transition-opacity cursor-pointer mb-3 ${userId.trim().length > 0 && password.trim().length > 0 ? "bg-active-button" : "bg-inactive-button"}`}
|
||||
>
|
||||
로그인
|
||||
</button>
|
||||
|
||||
{/* 하단 링크들 */}
|
||||
<div className="flex items-center justify-between text-[15px]">
|
||||
<div className="flex items-center justify-between text-[15px] leading-[150%] h-[36px]">
|
||||
<Link
|
||||
href="/register"
|
||||
className="underline-offset-2 text-basic-text"
|
||||
className="underline-offset-2 text-basic-text font-bold"
|
||||
>
|
||||
회원가입
|
||||
</Link>
|
||||
|
||||
@@ -1,613 +0,0 @@
|
||||
\"use client\";
|
||||
|
||||
import { useState } from \"react\";
|
||||
import Link from \"next/link\";
|
||||
import MainLogo from \"@/app/svgs/mainlogosvg\";
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [name, setName] = useState(\"\");
|
||||
const [phone, setPhone] = useState(\"\");
|
||||
const [email, setEmail] = useState(\"\");
|
||||
const [password, setPassword] = useState(\"\");
|
||||
const [confirmPassword, setConfirmPassword] = useState(\"\");
|
||||
const [gender, setGender] = useState<\"male\" | \"female\" | \"\">(\"\");
|
||||
const [birth, setBirth] = useState(\"\");
|
||||
|
||||
const [agreeAll, setAgreeAll] = useState(false);
|
||||
const [agreeAge, setAgreeAge] = useState(false);
|
||||
const [agreeTos, setAgreeTos] = useState(false);
|
||||
const [agreePrivacy, setAgreePrivacy] = useState(false);
|
||||
|
||||
function syncAgreeAll(nextAll: boolean) {
|
||||
setAgreeAll(nextAll);
|
||||
setAgreeAge(nextAll);
|
||||
setAgreeTos(nextAll);
|
||||
setAgreePrivacy(nextAll);
|
||||
}
|
||||
|
||||
function handleIndividualAgree(next: { age?: boolean; tos?: boolean; privacy?: boolean }) {
|
||||
const nextAge = next.age ?? agreeAge;
|
||||
const nextTos = next.tos ?? agreeTos;
|
||||
const nextPrivacy = next.privacy ?? agreePrivacy;
|
||||
setAgreeAge(nextAge);
|
||||
setAgreeTos(nextTos);
|
||||
setAgreePrivacy(nextPrivacy);
|
||||
setAgreeAll(nextAge && nextTos && nextPrivacy);
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
// TODO: 서버 연동 시 실제 제출 로직으로 교체
|
||||
console.log({
|
||||
name,
|
||||
phone,
|
||||
email,
|
||||
password,
|
||||
confirmPassword,
|
||||
gender,
|
||||
birth,
|
||||
agree: { all: agreeAll, age: agreeAge, tos: agreeTos, privacy: agreePrivacy },
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className=\"min-h-screen w-full flex flex-col items-center justify-between\">
|
||||
<div></div>
|
||||
<div className=\"rounded-xl bg-white max-w-[480px] w-full\">
|
||||
{/* 로고/타이틀 */}
|
||||
<div className=\"mb-10 flex flex-col items-center\">
|
||||
<div className=\"mb-[7px]\">
|
||||
<MainLogo />
|
||||
</div>
|
||||
<div className=\"text-[24px] font-[700] leading-[150%] text-neutral-700\">회원가입</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className=\"space-y-6\">
|
||||
<div className=\"space-y-4\">
|
||||
{/* 이름 */}
|
||||
<div className=\"space-y-2\">
|
||||
<label className=\"text-[15px] font-[600] text-[var(--color-basic-text)]\">이름</label>
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder=\"이름을 입력해 주세요.\"
|
||||
className=\"h-[40px] px-[12px] py-[8px] w-full rounded-[8px] border border-neutral-40 text-[16px] text-neutral-700 placeholder:text-input-placeholder-text focus:outline-none focus:border-neutral-700\"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 휴대폰 번호 */}
|
||||
<div className=\"space-y-2\">
|
||||
<label className=\"text-[15px] font-[600] text-[var(--color-basic-text)]\">휴대폰 번호</label>
|
||||
<input
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder=\"-없이 입력해 주세요.\"
|
||||
className=\"h-[40px] px-[12px] py-[8px] w-full rounded-[8px] border border-neutral-40 text-[16px] text-neutral-700 placeholder:text-input-placeholder-text focus:outline-none focus:border-neutral-700\"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 아이디(이메일) + 인증번호 전송 */}
|
||||
<div className=\"space-y-2\">
|
||||
<label className=\"text-[15px] font-[600] text-[var(--color-basic-text)]\">아이디 (이메일)</label>
|
||||
<div className=\"flex gap-2\">
|
||||
<input
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder=\"이메일을 입력해 주세요.\"
|
||||
className=\"h-[40px] px-[12px] py-[8px] w-full rounded-[8px] border border-neutral-40 text-[16px] text-neutral-700 placeholder:text-input-placeholder-text focus:outline-none focus:border-neutral-700\"
|
||||
/>
|
||||
<button
|
||||
type=\"button\"
|
||||
className=\"h-[40px] px-[12px] rounded-[8px] bg-[#f1f3f5] text-[16px] font-[600] text-[#4c5561]\"
|
||||
>
|
||||
인증번호 전송
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 비밀번호 */}
|
||||
<div className=\"space-y-2\">
|
||||
<label className=\"text-[15px] font-[600] text-[var(--color-basic-text)]\">비밀번호</label>
|
||||
<input
|
||||
type=\"password\"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder=\"8~16자의 영문/숫자/특수문자를 조합해서 입력해 주세요.\"
|
||||
className=\"h-[40px] px-[12px] py-[8px] w-full rounded-[8px] border border-neutral-40 text-[16px] text-neutral-700 placeholder:text-input-placeholder-text focus:outline-none focus:border-neutral-700\"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 비밀번호 확인 */}
|
||||
<div className=\"space-y-2\">
|
||||
<label className=\"text-[15px] font-[600] text-[var(--color-basic-text)]\">비밀번호 확인</label>
|
||||
<input
|
||||
type=\"password\"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder=\"비밀번호를 다시 입력해 주세요.\"
|
||||
className=\"h-[40px] px-[12px] py-[8px] w-full rounded-[8px] border border-neutral-40 text-[16px] text-neutral-700 placeholder:text-input-placeholder-text focus:outline-none focus:border-neutral-700\"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 성별 */}
|
||||
<div className=\"space-y-2\">
|
||||
<label className=\"text-[15px] font-[600] text-[var(--color-basic-text)]\">성별</label>
|
||||
<div className=\"flex items-center gap-5 h-[40px]\">
|
||||
<label className=\"flex items-center gap-2 text-[15px] text-neutral-700\">
|
||||
<input
|
||||
type=\"radio\"
|
||||
name=\"gender\"
|
||||
checked={gender === \"male\"}
|
||||
onChange={() => setGender(\"male\")}
|
||||
className=\"h-[18px] w-[18px] cursor-pointer\"
|
||||
style={{ accentColor: \"var(--color-input-border-select)\" }}
|
||||
/>
|
||||
남성
|
||||
</label>
|
||||
<label className=\"flex items-center gap-2 text-[15px] text-neutral-700\">
|
||||
<input
|
||||
type=\"radio\"
|
||||
name=\"gender\"
|
||||
checked={gender === \"female\"}
|
||||
onChange={() => setGender(\"female\")}
|
||||
className=\"h-[18px] w-[18px] cursor-pointer\"
|
||||
style={{ accentColor: \"var(--color-input-border-select)\" }}
|
||||
/>
|
||||
여성
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 생년월일 */}
|
||||
<div className=\"space-y-2\">
|
||||
<label className=\"text-[15px] font-[600] text-[var(--color-basic-text)]\">생년월일</label>
|
||||
<input
|
||||
type=\"date\"
|
||||
value={birth}
|
||||
onChange={(e) => setBirth(e.target.value)}
|
||||
className=\"h-[40px] px-[12px] py-[8px] w-full rounded-[8px] border border-neutral-40 text-[16px] text-neutral-700 focus:outline-none focus:border-neutral-700\"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 약관 */}
|
||||
<div className=\"space-y-4\">
|
||||
<label className=\"flex items-center gap-2 text-[18px] font-[600] text-neutral-700 cursor-pointer\">
|
||||
<input
|
||||
type=\"checkbox\"
|
||||
checked={agreeAll}
|
||||
onChange={(e) => syncAgreeAll(e.target.checked)}
|
||||
className=\"h-[20px] w-[20px] cursor-pointer rounded border\"
|
||||
style={{ accentColor: \"var(--color-input-border-select)\", borderColor: \"var(--color-inactive-checkbox)\" }}
|
||||
/>
|
||||
모든 항목에 동의합니다.
|
||||
</label>
|
||||
|
||||
<div className=\"h-px w-full bg-[var(--color-input-border)]\" />
|
||||
|
||||
<div className=\"space-y-3\">
|
||||
<label className=\"flex items-center justify-between text-[15px]\">
|
||||
<span className=\"flex items-center gap-2 text-neutral-700\">
|
||||
<input
|
||||
type=\"checkbox\"
|
||||
checked={agreeAge}
|
||||
onChange={(e) => handleIndividualAgree({ age: e.target.checked })}
|
||||
className=\"h-[18px] w-[18px] cursor-pointer rounded border\"
|
||||
style={{ accentColor: \"var(--color-input-border-select)\", borderColor: \"var(--color-inactive-checkbox)\" }}
|
||||
/>
|
||||
<span>
|
||||
만 14세 이상입니다. <span className=\"text-[#384fbf]\">(필수)</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className=\"flex items-center justify-between text-[15px]\">
|
||||
<span className=\"flex items-center gap-2 text-neutral-700\">
|
||||
<input
|
||||
type=\"checkbox\"
|
||||
checked={agreeTos}
|
||||
onChange={(e) => handleIndividualAgree({ tos: e.target.checked })}
|
||||
className=\"h-[18px] w-[18px] cursor-pointer rounded border\"
|
||||
style={{ accentColor: \"var(--color-input-border-select)\", borderColor: \"var(--color-inactive-checkbox)\" }}
|
||||
/>
|
||||
<span>
|
||||
이용 약관 동의 <span className=\"text-[#384fbf]\">(필수)</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className=\"flex items-center justify-between text-[15px]\">
|
||||
<span className=\"flex items-center gap-2 text-neutral-700\">
|
||||
<input
|
||||
type=\"checkbox\"
|
||||
checked={agreePrivacy}
|
||||
onChange={(e) => handleIndividualAgree({ privacy: e.target.checked })}
|
||||
className=\"h-[18px] w-[18px] cursor-pointer rounded border\"
|
||||
style={{ accentColor: \"var(--color-input-border-select)\", borderColor: \"var(--color-inactive-checkbox)\" }}
|
||||
/>
|
||||
<span>
|
||||
개인정보 수집 및 이용 동의 <span className=\"text-[#384fbf]\">(필수)</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 버튼들 */}
|
||||
<div className=\"flex gap-3\">
|
||||
<Link
|
||||
href=\"/login\"
|
||||
className=\"h-14 flex-1 rounded-[12px] bg-[#f1f3f5] text-center grid place-items-center text-[18px] font-[600] text-[#4c5561]\"
|
||||
>
|
||||
돌아가기
|
||||
</Link>
|
||||
<button
|
||||
type=\"submit\"
|
||||
className=\"h-14 flex-1 rounded-[12px] text-[18px] font-[600] text-white\"
|
||||
style={{ backgroundColor: \"var(--color-active-button)\" }}
|
||||
disabled={!(agreeAge && agreeTos && agreePrivacy)}
|
||||
>
|
||||
회원 가입 완료
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div></div>
|
||||
<p className=\"text-center text-[15px] text-basic-text\">
|
||||
Copyright ⓒ 2025 XL LMS. All rights reserved
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function RegisterPage() {
|
||||
// form states
|
||||
const [name, setName] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordConfirm, setPasswordConfirm] = useState("");
|
||||
const [gender, setGender] = useState<"male" | "female">("male");
|
||||
const [birth, setBirth] = useState("");
|
||||
|
||||
// agreements
|
||||
const [agreeAge, setAgreeAge] = useState(false);
|
||||
const [agreeTos, setAgreeTos] = useState(false);
|
||||
const [agreePrivacy, setAgreePrivacy] = useState(false);
|
||||
const agreeAll = useMemo(
|
||||
() => agreeAge && agreeTos && agreePrivacy,
|
||||
[agreeAge, agreeTos, agreePrivacy],
|
||||
);
|
||||
|
||||
function handleToggleAll(next: boolean) {
|
||||
setAgreeAge(next);
|
||||
setAgreeTos(next);
|
||||
setAgreePrivacy(next);
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
if (!agreeAge || !agreeTos || !agreePrivacy) {
|
||||
alert("필수 약관에 동의해 주세요.");
|
||||
return;
|
||||
}
|
||||
if (password.length < 8 || password.length > 16) {
|
||||
alert("비밀번호는 8~16자여야 합니다.");
|
||||
return;
|
||||
}
|
||||
if (password !== passwordConfirm) {
|
||||
alert("비밀번호가 일치하지 않습니다.");
|
||||
return;
|
||||
}
|
||||
// TODO: 실제 API 연동
|
||||
console.log({
|
||||
name,
|
||||
phone,
|
||||
email,
|
||||
password,
|
||||
gender,
|
||||
birth,
|
||||
agreements: { agreeAge, agreeTos, agreePrivacy },
|
||||
});
|
||||
alert("회원가입 폼이 제출되었습니다. (데모)");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full flex flex-col items-center justify-between">
|
||||
<div></div>
|
||||
<div className="rounded-xl bg-white max-w-[480px] w-full">
|
||||
{/* 헤더 타이틀 */}
|
||||
<div className="mb-10 flex flex-col items-center">
|
||||
<div className="text-[24px] font-[700] leading-[150%] text-neutral-700">
|
||||
회원가입
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 폼 */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
{/* 이름 */}
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="text-[15px] font-[600]"
|
||||
style={{ color: "var(--color-basic-text)" }}
|
||||
>
|
||||
이름
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="이름을 입력해 주세요."
|
||||
className="
|
||||
h-10 px-[12px] py-[8px] w-full rounded-[8px] border border-neutral-40
|
||||
focus:outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none
|
||||
focus:appearance-none focus:border-neutral-700
|
||||
text-[16px] text-neutral-700 font-[400] leading-[150%] placeholder:text-input-placeholder-text
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 휴대폰 번호 */}
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="text-[15px] font-[600]"
|
||||
style={{ color: "var(--color-basic-text)" }}
|
||||
>
|
||||
휴대폰 번호
|
||||
</label>
|
||||
<input
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="-없이 입력해 주세요."
|
||||
className="
|
||||
h-10 px-[12px] py-[8px] w-full rounded-[8px] border border-neutral-40
|
||||
focus:outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none
|
||||
focus:appearance-none focus:border-neutral-700
|
||||
text-[16px] text-neutral-700 font-[400] leading-[150%] placeholder:text-input-placeholder-text
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 아이디(이메일) + 인증번호 전송 */}
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="text-[15px] font-[600]"
|
||||
style={{ color: "var(--color-basic-text)" }}
|
||||
>
|
||||
아이디 (이메일)
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="이메일을 입력해 주세요."
|
||||
className="
|
||||
h-10 px-[12px] py-[8px] rounded-[8px] w-full border border-neutral-40
|
||||
focus:outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none
|
||||
focus:appearance-none focus:border-neutral-700
|
||||
text-[16px] text-neutral-700 font-[400] leading-[150%] placeholder:text-input-placeholder-text
|
||||
"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="h-10 w-[136px] rounded-[8px] text-[16px] font-[600] bg-[#f9fafb] text-[#b1b8c0]"
|
||||
>
|
||||
인증번호 전송
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 비밀번호 */}
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="text-[15px] font-[600]"
|
||||
style={{ color: "var(--color-basic-text)" }}
|
||||
>
|
||||
비밀번호
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="8~16자의 영문/숫자/특수문자를 조합해서 입력해 주세요."
|
||||
className="
|
||||
h-10 px-[12px] py-[8px] w-full rounded-[8px] border border-neutral-40
|
||||
focus:outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none
|
||||
focus:appearance-none focus:border-neutral-700
|
||||
text-[16px] text-neutral-700 font-[400] leading-[150%] placeholder:text-input-placeholder-text
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 비밀번호 확인 */}
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="passwordConfirm"
|
||||
className="text-[15px] font-[600]"
|
||||
style={{ color: "var(--color-basic-text)" }}
|
||||
>
|
||||
비밀번호 확인
|
||||
</label>
|
||||
<input
|
||||
id="passwordConfirm"
|
||||
name="passwordConfirm"
|
||||
type="password"
|
||||
value={passwordConfirm}
|
||||
onChange={(e) => setPasswordConfirm(e.target.value)}
|
||||
placeholder="비밀번호를 다시 입력해 주세요."
|
||||
className="
|
||||
h-10 px-[12px] py-[8px] w-full rounded-[8px] border border-neutral-40
|
||||
focus:outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none
|
||||
focus:appearance-none focus:border-neutral-700
|
||||
text-[16px] text-neutral-700 font-[400] leading-[150%] placeholder:text-input-placeholder-text
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 성별 */}
|
||||
<div className="space-y-2">
|
||||
div className="text-[15px] font-[600]" style={{ color: "var(--color-basic-text)" }}>
|
||||
성별
|
||||
</div>
|
||||
<div className="flex items-center gap-5 h-10">
|
||||
<label className="flex items-center gap-2 text-[15px] text-neutral-700">
|
||||
<input
|
||||
type="radio"
|
||||
name="gender"
|
||||
checked={gender === "male"}
|
||||
onChange={() => setGender("male")}
|
||||
className="h-[18px] w-[18px] rounded-full"
|
||||
style={{ accentColor: "var(--color-active-button)" }}
|
||||
/>
|
||||
남성
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-[15px] text-neutral-700">
|
||||
<input
|
||||
type="radio"
|
||||
name="gender"
|
||||
checked={gender === "female"}
|
||||
onChange={() => setGender("female")}
|
||||
className="h-[18px] w-[18px] rounded-full"
|
||||
style={{ accentColor: "var(--color-input-border)" }}
|
||||
/>
|
||||
여성
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 생년월일 */}
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="birth"
|
||||
className="text-[15px] font-[600]"
|
||||
style={{ color: "var(--color-basic-text)" }}
|
||||
>
|
||||
생년월일
|
||||
</label>
|
||||
<input
|
||||
id="birth"
|
||||
name="birth"
|
||||
type="date"
|
||||
value={birth}
|
||||
onChange={(e) => setBirth(e.target.value)}
|
||||
className="
|
||||
h-10 px-[12px] py-[8px] w-full rounded-[8px] border border-neutral-40
|
||||
focus:outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none
|
||||
focus:appearance-none focus:border-neutral-700
|
||||
text-[16px] text-neutral-700 font-[400] leading-[150%]
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 약관 동의 */}
|
||||
<div className="space-y-4">
|
||||
<label className="flex cursor-pointer select-none items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreeAll}
|
||||
onChange={(e) => handleToggleAll(e.target.checked)}
|
||||
className="h-[20px] w-[20px] cursor-pointer rounded border"
|
||||
style={{
|
||||
accentColor: "var(--color-input-border-select)",
|
||||
borderColor: "var(--color-inactive-checkbox)",
|
||||
}}
|
||||
/>
|
||||
<span className="text-[18px] font-[600] text-neutral-700">모든 항목에 동의합니다.</span>
|
||||
</label>
|
||||
<div className="h-px w-full bg-neutral-200" />
|
||||
<div className="space-y-3">
|
||||
<label className="flex cursor-pointer select-none items-center justify-between">
|
||||
<span className="flex items-center gap-2 text-[15px]" style={{ color: "var(--color-basic-text)" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreeAge}
|
||||
onChange={(e) => setAgreeAge(e.target.checked)}
|
||||
className="h-[20px] w-[20px] cursor-pointer rounded border"
|
||||
style={{
|
||||
accentColor: "var(--color-input-border-select)",
|
||||
borderColor: "var(--color-inactive-checkbox)",
|
||||
}}
|
||||
/>
|
||||
만 14세 이상입니다. <span className="text-[#384fbf]">(필수)</span>
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex cursor-pointer select-none items-center justify-between">
|
||||
<span className="flex items-center gap-2 text-[15px]" style={{ color: "var(--color-basic-text)" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreeTos}
|
||||
onChange={(e) => setAgreeTos(e.target.checked)}
|
||||
className="h-[20px] w-[20px] cursor-pointer rounded border"
|
||||
style={{
|
||||
accentColor: "var(--color-input-border-select)",
|
||||
borderColor: "var(--color-inactive-checkbox)",
|
||||
}}
|
||||
/>
|
||||
이용 약관 동의 <span className="text-[#384fbf]">(필수)</span>
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex cursor-pointer select-none items-center justify-between">
|
||||
<span className="flex items-center gap-2 text-[15px]" style={{ color: "var(--color-basic-text)" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreePrivacy}
|
||||
onChange={(e) => setAgreePrivacy(e.target.checked)}
|
||||
className="h-[20px] w-[20px] cursor-pointer rounded border"
|
||||
style={{
|
||||
accentColor: "var(--color-input-border-select)",
|
||||
borderColor: "var(--color-inactive-checkbox)",
|
||||
}}
|
||||
/>
|
||||
개인정보 수집 및 이용 동의 <span className="text-[#384fbf]">(필수)</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Link
|
||||
href="/login"
|
||||
className="h-14 rounded-[12px] bg-[#f1f3f5] text-[18px] font-[600] text-center flex items-center justify-center"
|
||||
style={{ color: "var(--color-basic-text)" }}
|
||||
>
|
||||
돌아가기
|
||||
</Link>
|
||||
<button
|
||||
type="submit"
|
||||
className="h-14 rounded-[12px] text-[18px] font-[600] text-white"
|
||||
style={{ backgroundColor: "var(--color-inactive-button)" }}
|
||||
>
|
||||
회원 가입 완료
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div></div>
|
||||
<p className="text-center text-[15px] text-basic-text">
|
||||
Copyright ⓒ 2025 XL LMS. All rights reserved
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
11
src/app/svgs/inputformx.tsx
Normal file
11
src/app/svgs/inputformx.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
export default function LoginInputSvg() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M10 17.5C5.8575 17.5 2.5 14.1425 2.5 10C2.5 5.8575 5.8575 2.5 10 2.5C14.1425 2.5 17.5 5.8575 17.5 10C17.5 14.1425 14.1425 17.5 10 17.5Z" fill="#6C7682" stroke="#6C7682" strokeWidth="1.875" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M12.3583 7.6416L7.6416 12.3583" stroke="white" strokeWidth="1.875" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M12.3583 12.3583L7.6416 7.6416" stroke="white" strokeWidth="1.875" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user