로그인관련

This commit is contained in:
2025-11-18 03:27:30 +09:00
parent 851fec4096
commit e574caa7ce
16 changed files with 1458 additions and 8 deletions

View File

@@ -0,0 +1,82 @@
"use client";
import React, { useState } from "react";
type FindIdDevOptionProps = {
doneEnabled?: boolean;
setDoneEnabled?: (enabled: boolean) => void;
failedEnabled?: boolean;
setFailedEnabled?: (enabled: boolean) => void;
};
export default function FindIdDevOption({
doneEnabled,
setDoneEnabled,
failedEnabled,
setFailedEnabled,
}: FindIdDevOptionProps) {
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">
<button
type="button"
aria-label="옵션 닫기"
className="absolute inset-0 bg-black/20 cursor-default"
onClick={() => setIsOpen(false)}
/>
<div className="w-[500px] h-[600px] flex bg-white/80 p-10 border rounded-lg relative">
<button
type="button"
aria-label="옵션 닫기"
onClick={() => setIsOpen(false)}
className="absolute top-3 right-3 w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 text-gray-700 flex items-center justify-center"
>
<span className="text-sm"></span>
</button>
<ul className="flex flex-col gap-4">
<li className="flex items-center justify-between">
<p className="mr-4">find-id done overlay</p>
<button
type="button"
aria-label="find-id done overlay 토글"
aria-pressed={!!doneEnabled}
onClick={() => setDoneEnabled?.(!doneEnabled)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${doneEnabled ? "bg-blue-600" : "bg-gray-300"}`}
>
<span
className={`inline-block h-5 w-5 transform rounded-full bg-white transition ${doneEnabled ? "translate-x-5" : "translate-x-1"}`}
/>
</button>
</li>
<li className="flex items-center justify-between">
<p className="mr-4">find-id failed overlay</p>
<button
type="button"
aria-label="find-id failed overlay 토글"
aria-pressed={!!failedEnabled}
onClick={() => setFailedEnabled?.(!failedEnabled)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${failedEnabled ? "bg-blue-600" : "bg-gray-300"}`}
>
<span
className={`inline-block h-5 w-5 transform rounded-full bg-white transition ${failedEnabled ? "translate-x-5" : "translate-x-1"}`}
/>
</button>
</li>
</ul>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,131 @@
"use client";
import React, { useMemo, useState } from "react";
import LoginInputSvg from "@/app/svgs/inputformx";
import Link from "next/link";
type FindIdOptionProps = {
onOpenDone: (userId?: string) => void;
onOpenFailed: () => void;
};
export default function FindIdOption({ onOpenDone, onOpenFailed }: FindIdOptionProps) {
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [focused, setFocused] = useState<Record<string, boolean>>({});
const [errors, setErrors] = useState<Record<string, string>>({});
const isNameValid = useMemo(() => name.trim().length > 0, [name]);
const isPhoneValid = useMemo(() => /^\d{9,11}$/.test(phone), [phone]);
const canSubmit = useMemo(() => isNameValid && isPhoneValid, [isNameValid, isPhoneValid]);
function validateAll() {
const next: Record<string, string> = {};
if (!isNameValid) next.name = "이름을 입력해 주세요.";
if (!isPhoneValid) next.phone = "휴대폰 번호를 숫자만 9~11자리로 입력해 주세요.";
setErrors(next);
return Object.keys(next).length === 0;
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
if (!validateAll()) return;
const mockUserId = `${name.trim()}@example.com`;
onOpenDone(mockUserId);
}
return (
<div className="rounded-xl bg-white max-w-[560px] px-[40px] w-full relative">
<Link
href="/login"
aria-label="닫기"
className="absolute top-3 right-3 w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 text-gray-700 flex items-center justify-center"
>
<span className="text-sm"></span>
</Link>
<div className="my-15 flex flex-col items-center">
<div className="text-[24px] font-extrabold leading-[150%] text-neutral-700">
</div>
<p className="text-[18px] leading-[150%] text-[#6c7682] mt-[8px] text-center">
.
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<label htmlFor="name" className="text-[15px] font-semibold text-[#6c7682]">
</label>
<div className="relative">
<input
id="name"
name="name"
value={name}
onChange={(e) => setName(e.target.value)}
onFocus={() => setFocused((p) => ({ ...p, name: true }))}
onBlur={() => setFocused((p) => ({ ...p, name: 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]"
/>
{name.trim().length > 0 && focused.name && (
<button
type="button"
onMouseDown={(e) => {
e.preventDefault();
setName("");
}}
aria-label="입력 지우기"
className="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer">
<LoginInputSvg />
</button>
)}
</div>
{errors.name && <p className="text-error text-[13px] leading-tight">{errors.name}</p>}
</div>
<div className="space-y-2">
<label htmlFor="phone" className="text-[15px] font-semibold text-[#6c7682]">
</label>
<div className="relative">
<input
id="phone"
name="phone"
type="tel"
inputMode="numeric"
value={phone}
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 && (
<button
type="button"
onMouseDown={(e) => {
e.preventDefault();
setPhone("");
}}
aria-label="입력 지우기"
className="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer">
<LoginInputSvg />
</button>
)}
</div>
{errors.phone && <p className="text-error text-[13px] leading-tight">{errors.phone}</p>}
</div>
<button
type="submit"
className={`h-[40px] w-full rounded-[12px] text-[18px] font-semibold text-white ${canSubmit ? "bg-active-button" : "bg-inactive-button"} cursor-pointer`}>
</button>
</form>
</div>
);
}

View File

@@ -0,0 +1,93 @@
"use client";
import React from "react";
import Link from "next/link";
type IdFindDoneProps = {
on: boolean;
onClose?: () => void;
children?: React.ReactNode;
title?: string;
description?: string;
primaryHref?: string;
primaryText?: string;
userId?: string;
secondaryHref?: string;
secondaryText?: string;
joinedAtText?: string;
};
export default function IdFindDone({
on,
onClose,
children,
title = "아이디 확인",
description = "회원님의 정보와 일치하는 아이디 목록입니다.",
primaryHref = "/login",
primaryText = "로그인",
userId = "test@example.com",
secondaryHref = "/reset-password",
secondaryText = "비밀번호 재설정",
joinedAtText = "가입일: 2025년 10월 17일",
}: IdFindDoneProps) {
if (!on) return null;
return (
<div className="fixed inset-0 z-50 bg-white">
<div className="absolute inset-0 overflow-auto">
<div className="min-h-screen w-full flex flex-col items-center justify-center p-6">
{children ? (
children
) : (
<div className="w-full max-w-[480px] text-center">
<div className="space-y-4">
<div className="text-[24px] font-extrabold leading-[150%] text-neutral-700">
{title}
</div>
<p className="text-[18px] leading-[150%] text-[#6c7682] whitespace-pre-line">
{description}
</p>
</div>
<div className="mt-[60px]">
<div className="bg-[#f9fafb] border border-neutral-40 rounded-[16px] py-[24px]">
{userId && (
<p className="text-[18px] font-semibold text-neutral-700">
{userId}
</p>
)}
{joinedAtText && (
<p className="text-[16px] text-[#6c7682] mt-2">
({joinedAtText})
</p>
)}
</div>
</div>
<div className="mt-[24px] flex gap-3">
<Link
href={secondaryHref}
className="h-[56px] flex-1 rounded-[12px] bg-[#f1f3f5] text-[18px] font-semibold text-basic-text flex items-center justify-center"
>
{secondaryText}
</Link>
<Link
href={primaryHref}
className="h-[56px] flex-1 rounded-[12px] bg-active-button text-white text-[18px] font-semibold flex items-center justify-center"
>
{primaryText}
</Link>
</div>
</div>
)}
</div>
</div>
{onClose && (
<button
type="button"
aria-label="닫기"
className="absolute inset-0 -z-10 cursor-default"
onClick={onClose}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,71 @@
"use client";
import React from "react";
import Link from "next/link";
type IdFindFailedProps = {
on: boolean;
onClose?: () => void;
children?: React.ReactNode;
title?: string;
description?: string;
primaryHref?: string;
primaryText?: string;
};
/**
* 아이디 찾기 실패 단일 페이지(전체 덮기) 컴포넌트.
* - 기본 텍스트/버튼 제공. 필요 시 children으로 완전 대체 가능.
* - 디자인 토큰은 globals.css (@theme inline)을 사용.
*/
export default function IdFindFailed({
on,
onClose,
children,
title = "아이디 확인",
description = "회원님의 정보로 가입된 아이디가 없습니다.\n회원가입을 진행해 주세요.",
primaryHref = "/register",
primaryText = "회원가입",
}: IdFindFailedProps) {
if (!on) return null;
return (
<div className="fixed inset-0 z-50 bg-white">
<div className="absolute inset-0 overflow-auto">
<div className="min-h-screen w-full flex flex-col items-center justify-center p-6">
{children ? (
children
) : (
<div className="w-full max-w-[480px] text-center">
<div className="text-[24px] font-extrabold leading-[150%] text-neutral-700 mb-4">
{title}
</div>
<p className="text-[18px] text-[#6c7682] mb-[60px] whitespace-pre-line">
{description}
</p>
<div className="flex gap-3">
<Link
href={primaryHref}
className="h-[56px] flex-1 rounded-[12px] bg-active-button text-white text-[18px] font-semibold flex items-center justify-center"
>
{primaryText}
</Link>
</div>
</div>
)}
</div>
</div>
{onClose && (
<button
type="button"
aria-label="닫기"
className="absolute inset-0 -z-10 cursor-default"
onClick={onClose}
/>
)}
</div>
);
}

50
src/app/find-id/page.tsx Normal file
View File

@@ -0,0 +1,50 @@
"use client";
import { useState } from "react";
import IdFindDone from "./IdFindDone";
import IdFindFailed from "./IdFindFailed";
import FindIdOption from "./FindIdOption";
import FindIdDevOption from "./FindIdDevOption";
export default function FindIdPage() {
const [isDoneOpen, setIsDoneOpen] = useState(false);
const [isFailedOpen, setIsFailedOpen] = useState(false);
const [foundUserId, setFoundUserId] = useState<string | undefined>(undefined);
return (
<div className="min-h-screen w-full flex flex-col items-center justify-between">
<IdFindDone
on={isDoneOpen}
userId={foundUserId}
onClose={() => setIsDoneOpen(false)}
/>
<IdFindFailed
on={isFailedOpen}
onClose={() => setIsFailedOpen(false)}
/>
<FindIdOption
onOpenDone={(id) => {
setFoundUserId(id);
setIsDoneOpen(true);
}}
onOpenFailed={() => {
setIsFailedOpen(true);
}}
/>
<FindIdDevOption
doneEnabled={isDoneOpen}
setDoneEnabled={setIsDoneOpen}
failedEnabled={isFailedOpen}
setFailedEnabled={setIsFailedOpen}
/>
<p className="text-center py-[40px] text-[15px] text-basic-text">
Copyright 2025 XL LMS. All rights reserved
</p>
</div>
);
}