This commit is contained in:
2025-10-29 22:03:43 +09:00
parent a96ee0fb60
commit 8d79468bf8
9 changed files with 424 additions and 15 deletions

2
.cursor/commands/ck.md Normal file
View File

@@ -0,0 +1,2 @@
방금 수행한 작업 체크

1
.cursor/commands/cm.md Normal file
View File

@@ -0,0 +1 @@
지금 수행한 작업 메시지 넣고 커밋s

View File

@@ -1 +1 @@
안녕?
다음 작업 수행

View File

@@ -1,11 +1,4 @@
# 로그인 페이지 작업 리스트
0. [x] 로그인 페이지 생성
레이아웃 중앙 정렬 설정
상단 로고 추가
아이디 입력폼 추가
한행에 아이디 기억하기 체크박스 추가
한행에 자동로그인 체크박스 추가
로그인 버튼 추가
한행으로 회원가입 버튼, 아이디찾기 버튼, 비밀번호 재설정 버튼 추가
최하단에 카피라이트 추가
1. [ ] dffd
2. [ ] df df

View File

View File

@@ -3,11 +3,23 @@
:root {
--background: #ffffff;
--foreground: #171717;
/* 테마 컬러 */
--color-primary: #3b82f6; /* Primary - 파란색 (메인 액션) */
--color-secondary: #8b5cf6; /* Secondary - 보라색 (보조 액션) */
--color-success: #10b981; /* Success - 초록색 (성공 상태) */
--color-error: #ef4444; /* Error - 빨간색 (에러/경고) */
--color-neutral: #6b7280; /* Neutral - 회색 (중립/보조) */
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--color-primary);
--color-secondary: var(--color-secondary);
--color-success: var(--color-success);
--color-error: var(--color-error);
--color-neutral: var(--color-neutral);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}

316
app/register/page.tsx Normal file
View File

@@ -0,0 +1,316 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
export default function RegisterPage() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
passwordConfirm: '',
phone: '',
});
const [errors, setErrors] = useState({
name: '',
email: '',
password: '',
passwordConfirm: '',
phone: '',
});
// 입력 필드 변경 핸들러
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
// 에러 초기화
if (errors[name as keyof typeof errors]) {
setErrors((prev) => ({
...prev,
[name]: '',
}));
}
};
// 이메일 유효성 검사
const validateEmail = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
// 비밀번호 유효성 검사 (최소 8자, 영문/숫자 조합)
const validatePassword = (password: string) => {
return password.length >= 8 && /[a-zA-Z]/.test(password) && /[0-9]/.test(password);
};
// 전화번호 유효성 검사
const validatePhone = (phone: string) => {
if (phone === '') return true; // 선택 항목
const phoneRegex = /^010-\d{4}-\d{4}$/;
return phoneRegex.test(phone);
};
// 폼 제출 전 유효성 검사
const validateForm = () => {
const newErrors = {
name: '',
email: '',
password: '',
passwordConfirm: '',
phone: '',
};
let isValid = true;
// 이름 검사
if (formData.name.trim() === '') {
newErrors.name = '이름을 입력해주세요.';
isValid = false;
}
// 이메일 검사
if (formData.email.trim() === '') {
newErrors.email = '이메일을 입력해주세요.';
isValid = false;
} else if (!validateEmail(formData.email)) {
newErrors.email = '올바른 이메일 형식이 아닙니다.';
isValid = false;
}
// 비밀번호 검사
if (formData.password === '') {
newErrors.password = '비밀번호를 입력해주세요.';
isValid = false;
} else if (!validatePassword(formData.password)) {
newErrors.password = '비밀번호는 8자 이상, 영문과 숫자를 포함해야 합니다.';
isValid = false;
}
// 비밀번호 확인 검사
if (formData.passwordConfirm === '') {
newErrors.passwordConfirm = '비밀번호 확인을 입력해주세요.';
isValid = false;
} else if (formData.password !== formData.passwordConfirm) {
newErrors.passwordConfirm = '비밀번호가 일치하지 않습니다.';
isValid = false;
}
// 전화번호 검사 (선택 항목)
if (formData.phone && !validatePhone(formData.phone)) {
newErrors.phone = '올바른 전화번호 형식이 아닙니다. (예: 010-1234-5678)';
isValid = false;
}
setErrors(newErrors);
return isValid;
};
// 전화번호 자동 포맷팅
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let value = e.target.value.replace(/[^\d]/g, '');
if (value.length > 11) value = value.slice(0, 11);
if (value.length > 7) {
value = value.slice(0, 3) + '-' + value.slice(3, 7) + '-' + value.slice(7);
} else if (value.length > 3) {
value = value.slice(0, 3) + '-' + value.slice(3);
}
setFormData((prev) => ({
...prev,
phone: value,
}));
};
// 폼 제출 핸들러
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
// 회원가입 완료 페이지로 이동
// 실제로는 API 호출을 하여 회원가입 처리를 해야 합니다
window.location.href = '/registercomplete';
}
};
// 필수 항목 입력 여부 확인
const canProceed =
formData.name.trim() !== '' &&
formData.email.trim() !== '' &&
formData.password !== '' &&
formData.passwordConfirm !== '';
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<div className="w-full max-w-2xl px-4">
{/* 회원가입 카드 */}
<div className="bg-white rounded-lg shadow-lg p-8">
{/* 제목 */}
<h1 className="text-2xl font-bold text-center mb-8"></h1>
{/* 단계 표시 */}
<div className="flex items-center justify-center mb-8 text-sm">
<div className="flex items-center">
<span className="px-3 py-1 text-gray-500">01 </span>
<span className="mx-2 text-gray-400">&gt;</span>
<span className="px-3 py-1 bg-blue-500 text-white rounded-md font-semibold">
02
</span>
<span className="mx-2 text-gray-400">&gt;</span>
<span className="px-3 py-1 text-gray-500">03 </span>
</div>
</div>
{/* 회원정보 입력 폼 */}
<form onSubmit={handleSubmit} className="space-y-4">
{/* 이름 */}
<div>
<label htmlFor="name" className="block text-sm font-medium mb-1">
<span className="text-red-500">*</span>
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className={`w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-2 ${
errors.name
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:ring-blue-500'
}`}
placeholder="이름을 입력하세요"
/>
{errors.name && (
<p className="mt-1 text-sm text-red-500">{errors.name}</p>
)}
</div>
{/* 이메일 */}
<div>
<label htmlFor="email" className="block text-sm font-medium mb-1">
<span className="text-red-500">*</span>
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={`w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-2 ${
errors.email
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:ring-blue-500'
}`}
placeholder="이메일을 입력하세요"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-500">{errors.email}</p>
)}
</div>
{/* 비밀번호 */}
<div>
<label htmlFor="password" className="block text-sm font-medium mb-1">
<span className="text-red-500">*</span>
</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={`w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-2 ${
errors.password
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:ring-blue-500'
}`}
placeholder="8자 이상, 영문과 숫자 조합"
/>
{errors.password && (
<p className="mt-1 text-sm text-red-500">{errors.password}</p>
)}
</div>
{/* 비밀번호 확인 */}
<div>
<label htmlFor="passwordConfirm" className="block text-sm font-medium mb-1">
<span className="text-red-500">*</span>
</label>
<input
type="password"
id="passwordConfirm"
name="passwordConfirm"
value={formData.passwordConfirm}
onChange={handleChange}
className={`w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-2 ${
errors.passwordConfirm
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:ring-blue-500'
}`}
placeholder="비밀번호를 다시 입력하세요"
/>
{errors.passwordConfirm && (
<p className="mt-1 text-sm text-red-500">{errors.passwordConfirm}</p>
)}
</div>
{/* 전화번호 */}
<div>
<label htmlFor="phone" className="block text-sm font-medium mb-1">
<span className="text-gray-500 text-xs">()</span>
</label>
<input
type="text"
id="phone"
name="phone"
value={formData.phone}
onChange={handlePhoneChange}
className={`w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-2 ${
errors.phone
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:ring-blue-500'
}`}
placeholder="010-1234-5678"
maxLength={13}
/>
{errors.phone && (
<p className="mt-1 text-sm text-red-500">{errors.phone}</p>
)}
</div>
{/* 버튼 영역 */}
<div className="mt-8 flex space-x-4">
<Link
href="/registeragreement"
className="flex-1 py-3 bg-gray-200 text-gray-700 rounded-md text-center hover:bg-gray-300 transition font-medium"
>
</Link>
<button
type="submit"
disabled={!canProceed}
className={`flex-1 py-3 rounded-md font-medium transition ${
canProceed
? 'bg-blue-500 text-white hover:bg-blue-600'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
}`}
>
</button>
</div>
</form>
</div>
{/* 카피라이트 */}
<div className="mt-8 text-center text-sm text-gray-500">
Copyright © 2025 XL LMS. All rights reserved
</div>
</div>
</div>
);
}

View File

@@ -147,16 +147,17 @@ export default function RegisterAgreementPage() {
>
</Link>
<button
disabled={!canProceed}
className={`flex-1 py-3 rounded-md font-medium transition ${
<Link
href={canProceed ? '/register' : '#'}
className={`flex-1 py-3 rounded-md text-center font-medium transition ${
canProceed
? 'bg-blue-500 text-white hover:bg-blue-600'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
: 'bg-gray-300 text-gray-500 cursor-not-allowed pointer-events-none'
}`}
onClick={(e) => !canProceed && e.preventDefault()}
>
</button>
</Link>
</div>
</div>

View File

@@ -0,0 +1,84 @@
'use client';
import Link from 'next/link';
export default function RegisterCompletePage() {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<div className="w-full max-w-2xl px-4">
{/* 회원가입 완료 카드 */}
<div className="bg-white rounded-lg shadow-lg p-8">
{/* 제목 */}
<h1 className="text-2xl font-bold text-center mb-8"></h1>
{/* 단계 표시 */}
<div className="flex items-center justify-center mb-8 text-sm">
<div className="flex items-center">
<span className="px-3 py-1 text-gray-500">01 </span>
<span className="mx-2 text-gray-400">&gt;</span>
<span className="px-3 py-1 text-gray-500">02 </span>
<span className="mx-2 text-gray-400">&gt;</span>
<span className="px-3 py-1 bg-blue-500 text-white rounded-md font-semibold">
03
</span>
</div>
</div>
{/* 완료 메시지 */}
<div className="text-center py-12">
{/* 체크 아이콘 */}
<div className="mb-6 flex justify-center">
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center">
<svg
className="w-12 h-12 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
</div>
{/* 완료 메시지 */}
<h2 className="text-2xl font-bold mb-4 text-gray-800">
!
</h2>
<p className="text-gray-600 mb-8">
XL LMS에 .
<br />
.
</p>
{/* 버튼 영역 */}
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/login"
className="px-8 py-3 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition font-medium text-center"
>
</Link>
<Link
href="/"
className="px-8 py-3 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition font-medium text-center"
>
</Link>
</div>
</div>
</div>
{/* 카피라이트 */}
<div className="mt-8 text-center text-sm text-gray-500">
Copyright © 2025 XL LMS. All rights reserved
</div>
</div>
</div>
);
}