feat: 랜딩페이지 완성 및 에디터 홈 버튼 기능 구현
주요 변경사항: - 🎨 가격 섹션을 CTA 섹션과 동일한 그라디언트 스타일로 변경 - 🗑️ 중복된 CTA 섹션 제거 및 페이지 구성 최적화 - 🏠 에디터 상단 로고 클릭 시 홈 이동 기능 추가 (저장 경고 포함) - 📱 인증 페이지 컴포넌트 추가 (SignIn/SignUp) - 💰 가격 정보 섹션 및 FAQ 섹션 추가 - 🔧 TopBar 컴포넌트에 로고 클릭 핸들러 추가 UI/UX 개선: - 가격 섹션: 파란색-보라색 그라디언트 배경 적용 - 카드 스타일: 반투명 배경 및 backdrop-blur 효과 - 텍스트 색상: 그라디언트 배경에 맞는 흰색/파란색 톤 적용 - 버튼 스타일: 인기 플랜 노란색 강조, 일반 플랜 반투명 스타일 기능 추가: - 에디터에서 로고 클릭 시 작업 손실 경고 및 홈 이동 - 완전한 인증 플로우 UI 구성 - 반응형 가격 정보 표시
This commit is contained in:
269
src/components/auth/SignInPage.tsx
Normal file
269
src/components/auth/SignInPage.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
interface SignInPageProps {
|
||||
className?: string;
|
||||
onSignIn?: (email: string, password: string) => void;
|
||||
onSignUpClick?: () => void;
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 화면 컴포넌트
|
||||
* Vooster.ai 스타일의 세련된 디자인
|
||||
* 실제 API 연동은 나중에 구현
|
||||
*/
|
||||
const SignInPage = React.forwardRef<HTMLDivElement, SignInPageProps>(
|
||||
({ className, onSignIn, onSignUpClick, onBack, ...props }, ref) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errors, setErrors] = useState<{
|
||||
email?: string;
|
||||
password?: string;
|
||||
general?: string;
|
||||
}>({});
|
||||
|
||||
// 이메일 유효성 검사
|
||||
const validateEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
// 폼 제출 핸들러
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 유효성 검사
|
||||
const newErrors: typeof errors = {};
|
||||
|
||||
if (!email) {
|
||||
newErrors.email = "이메일을 입력해주세요.";
|
||||
} else if (!validateEmail(email)) {
|
||||
newErrors.email = "올바른 이메일 형식을 입력해주세요.";
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
newErrors.password = "비밀번호를 입력해주세요.";
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// 로그인 처리 (실제 API 호출은 나중에)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500)); // 시뮬레이션
|
||||
onSignIn?.(email, password);
|
||||
} catch (error) {
|
||||
console.error("로그인 실패:", error);
|
||||
setErrors({
|
||||
general: "로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 flex items-center justify-center p-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{/* Background decoration */}
|
||||
<div className="absolute inset-0 bg-grid-slate-100 [mask-image:linear-gradient(0deg,transparent,black)]" />
|
||||
|
||||
<div className="relative w-full max-w-md">
|
||||
{/* 로고 및 헤더 */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
sheetEasy AI
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
계정에 로그인하여 AI 편집을 계속하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 로그인 폼 */}
|
||||
<Card className="shadow-xl border-0 bg-white/95 backdrop-blur-sm">
|
||||
<CardHeader className="text-center pb-6">
|
||||
<CardTitle className="text-2xl font-semibold text-gray-900">
|
||||
로그인
|
||||
</CardTitle>
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
계정에 다시 오신 것을 환영합니다
|
||||
</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
{/* 일반 오류 메시지 */}
|
||||
{errors.general && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-sm text-red-600">{errors.general}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* 이메일 입력 */}
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="text-sm font-medium text-gray-700"
|
||||
>
|
||||
이메일 주소
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={cn(
|
||||
"w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors",
|
||||
errors.email ? "border-red-500" : "border-gray-300",
|
||||
)}
|
||||
placeholder="your@email.com"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-red-600">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 비밀번호 입력 */}
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="text-sm font-medium text-gray-700"
|
||||
>
|
||||
비밀번호
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={cn(
|
||||
"w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors",
|
||||
errors.password ? "border-red-500" : "border-gray-300",
|
||||
)}
|
||||
placeholder="비밀번호를 입력해주세요"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="text-sm text-red-600">{errors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 비밀번호 찾기 링크 */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-blue-600 hover:text-blue-700 hover:underline"
|
||||
disabled={true} // 나중에 구현
|
||||
>
|
||||
비밀번호를 잊으셨나요? (준비중)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 로그인 버튼 */}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-green-500 to-blue-600 hover:from-green-600 hover:to-blue-700 text-white py-3 text-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-300"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||
로그인 중...
|
||||
</div>
|
||||
) : (
|
||||
"로그인"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* 소셜 로그인 (나중에 구현) */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-white text-gray-500">또는</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full py-3 text-gray-700 border-gray-300 hover:bg-gray-50"
|
||||
disabled={true} // 나중에 구현
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
Google로 로그인 (준비중)
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 가입 링크 */}
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
아직 계정이 없으신가요?{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSignUpClick}
|
||||
className="text-blue-600 hover:text-blue-700 font-medium hover:underline"
|
||||
>
|
||||
무료로 가입하기
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 뒤로가기 버튼 */}
|
||||
<div className="text-center mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
className="text-gray-600 hover:text-gray-700 text-sm hover:underline"
|
||||
>
|
||||
← 메인화면으로 돌아가기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SignInPage.displayName = "SignInPage";
|
||||
|
||||
export { SignInPage };
|
||||
Reference in New Issue
Block a user