From 851fec4096b84107be4362b0131a94fae5d8346f Mon Sep 17 00:00:00 2001 From: koreacomp5 Date: Tue, 18 Nov 2025 01:37:56 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/login/LoginErrorModal.tsx | 46 +++ src/app/login/loginoption.tsx | 53 +++ src/app/login/page.tsx | 93 ++++- src/app/register/page.tsx | 613 ------------------------------ src/app/svgs/inputformx.tsx | 11 + 5 files changed, 188 insertions(+), 628 deletions(-) create mode 100644 src/app/login/LoginErrorModal.tsx create mode 100644 src/app/login/loginoption.tsx create mode 100644 src/app/svgs/inputformx.tsx diff --git a/src/app/login/LoginErrorModal.tsx b/src/app/login/LoginErrorModal.tsx new file mode 100644 index 0000000..5713089 --- /dev/null +++ b/src/app/login/LoginErrorModal.tsx @@ -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 ( +
+ +
+ + + ); +} + + diff --git a/src/app/login/loginoption.tsx b/src/app/login/loginoption.tsx new file mode 100644 index 0000000..2128823 --- /dev/null +++ b/src/app/login/loginoption.tsx @@ -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 ( +
+ + { isOpen && ( +
+
+
    +
  • +

    login error modal

    + +
  • +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index f6b0d69..d45749a 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -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) { 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 (
-
+ + setIsLoginErrorOpen(false)} + /> + setIsLoginErrorOpen(true)} + loginErrorModalEnabled={isLoginErrorOpen} + setLoginErrorModalEnabled={setIsLoginErrorOpen} + /> +
{/* 로고 영역 */} -
+
@@ -31,10 +54,10 @@ export default function LoginPage() {
{/* 폼 */} -
+
{/* 아이디 */} -
+
@@ -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 && ( + + )}
{/* 비밀번호 */} -
+
@@ -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 && ( + + )}
{/* 체크박스들 */} -
+
@@ -101,16 +164,16 @@ export default function LoginPage() { {/* 로그인 버튼 */} {/* 하단 링크들 */} -
+
회원가입 diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx index 0ec830c..e69de29 100644 --- a/src/app/register/page.tsx +++ b/src/app/register/page.tsx @@ -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) { - e.preventDefault(); - // TODO: 서버 연동 시 실제 제출 로직으로 교체 - console.log({ - name, - phone, - email, - password, - confirmPassword, - gender, - birth, - agree: { all: agreeAll, age: agreeAge, tos: agreeTos, privacy: agreePrivacy }, - }); - } - - return ( -
-
-
- {/* 로고/타이틀 */} -
-
- -
-
회원가입
-
- - -
- {/* 이름 */} -
- - 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\" - /> -
- - {/* 휴대폰 번호 */} -
- - 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\" - /> -
- - {/* 아이디(이메일) + 인증번호 전송 */} -
- -
- 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\" - /> - -
-
- - {/* 비밀번호 */} -
- - 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\" - /> -
- - {/* 비밀번호 확인 */} -
- - 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\" - /> -
- - {/* 성별 */} -
- -
- - -
-
- - {/* 생년월일 */} -
- - 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\" - /> -
-
- - {/* 약관 */} -
- - -
- -
- - - - - -
-
- - {/* 버튼들 */} -
- - 돌아가기 - - -
- -
-
-

- Copyright ⓒ 2025 XL LMS. All rights reserved -

-
- ); -} - -"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) { - 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 ( -
-
-
- {/* 헤더 타이틀 */} -
-
- 회원가입 -
-
- - {/* 폼 */} -
-
- {/* 이름 */} -
- - 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 - " - /> -
- - {/* 휴대폰 번호 */} -
- - 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 - " - /> -
- - {/* 아이디(이메일) + 인증번호 전송 */} -
- -
- 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 - " - /> - -
-
- - {/* 비밀번호 */} -
- - 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 - " - /> -
- - {/* 비밀번호 확인 */} -
- - 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 className="text-[15px] font-[600]" style={{ color: "var(--color-basic-text)" }}> - 성별 -
-
- - -
-
- - {/* 생년월일 */} -
- - 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%] - " - /> -
-
- - {/* 약관 동의 */} -
- -
-
- - - -
-
- - {/* 하단 버튼 */} -
- - 돌아가기 - - -
- -
-
-

- Copyright ⓒ 2025 XL LMS. All rights reserved -

-
- ); -} - - diff --git a/src/app/svgs/inputformx.tsx b/src/app/svgs/inputformx.tsx new file mode 100644 index 0000000..e32de15 --- /dev/null +++ b/src/app/svgs/inputformx.tsx @@ -0,0 +1,11 @@ +"use client"; + +export default function LoginInputSvg() { + return ( + + + + + + ); +}