Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat: 로그인 및 회원가입 페이지 구현 #47

Merged
merged 14 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { lazy, Suspense, useEffect } from "react";
import { Routes, Route } from "react-router-dom";

import LoadingPage from "@/pages/Loading.tsx";
import Loading from "@/pages/Loading.tsx";
import NotFound from "@/pages/NotFound";
import SignIn from "@/pages/SignIn.tsx";
import SignUp from "@/pages/SignUp.tsx";

import { useMediaQuery } from "@/hooks/common/useMediaQuery";

Expand Down Expand Up @@ -36,31 +38,47 @@ export default function App() {
<Route
path="/"
element={
<Suspense fallback={<LoadingPage />}>
<Suspense fallback={<Loading />}>
<Home />
</Suspense>
}
/>
<Route
path="/admin"
element={
<Suspense fallback={<LoadingPage />}>
<Suspense fallback={<Loading />}>
<Admin />
</Suspense>
}
/>
<Route
path="/about"
element={
<Suspense fallback={<LoadingPage />}>
<Suspense fallback={<Loading />}>
<AboutService />
</Suspense>
}
/>
<Route
path="/signin"
element={
<Suspense fallback={<Loading />}>
<SignIn />
</Suspense>
}
/>
<Route
path="/signup"
element={
<Suspense fallback={<Loading />}>
<SignUp />
</Suspense>
}
/>
<Route
path="*"
element={
<Suspense fallback={<LoadingPage />}>
<Suspense fallback={<Loading />}>
<NotFound />
</Suspense>
}
Expand Down
56 changes: 56 additions & 0 deletions client/src/components/auth/AuthBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useLocation } from "react-router-dom";

import { motion } from "framer-motion";

export const AuthBanner = () => {
const location = useLocation();
const skipImageAnimation = location.state?.from === "/signin" || location.state?.from === "/signup";

return (
<motion.div
className="relative h-full w-full"
initial={skipImageAnimation ? false : { x: -100, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ duration: 0.5 }}
style={{
backgroundImage: `url('https://images.unsplash.com/photo-1735534151807-17f107a64cf6?q=80&w=3072&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
<div className="absolute inset-0 bg-black/40" />

<div className="relative flex h-full flex-col justify-end p-20">
<motion.h1
className="text-4xl md:text-5xl lg:text-6xl font-bold text-white max-w-xl"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<motion.span initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5, delay: 0.6 }}>
Write
</motion.span>{" "}
<motion.span initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5, delay: 0.8 }}>
Today,
</motion.span>
<br />
<motion.span initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5, delay: 1 }}>
Shape
</motion.span>{" "}
<motion.span initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5, delay: 1.2 }}>
Tomorrow
</motion.span>
</motion.h1>

<motion.p
className="mt-4 max-w-md text-xl text-white/90"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 1.4 }}
>
당신의 성장을 기록하는 공간
</motion.p>
junyeokk marked this conversation as resolved.
Show resolved Hide resolved
</div>
</motion.div>
);
};
21 changes: 21 additions & 0 deletions client/src/components/auth/AuthCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx";

interface AuthCardProps {
title: string;
description: string;
children: React.ReactNode;
}

export const AuthCard = ({ title, description, children }: AuthCardProps) => {
return (
<div className="flex h-full items-center justify-center">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-2xl font-semibold">{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
</div>
);
};
37 changes: 37 additions & 0 deletions client/src/components/auth/AuthSignInForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useLocation, useNavigate } from "react-router-dom";

import { AuthCard } from "@/components/auth/AuthCard.tsx";
import { AuthSocialLoginButtons } from "@/components/auth/AuthSocialLoginButtons.tsx";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

export const AuthSignInForm = () => {
const navigate = useNavigate();
const location = useLocation();

return (
<>
<AuthCard title="로그인" description="로그인을 해주세요">
<form className="space-y-4">
<div className="space-y-2">
<Input type="email" placeholder="이메일을 입력하세요" required />
<Input type="password" placeholder="비밀번호를 입력하세요" required />
</div>
<Button className="w-full" type="submit">
로그인
</Button>
</form>
<AuthSocialLoginButtons />
<div className="mt-4">
<Button
variant="link"
className="text-muted-foreground underline underline-offset-4 h-auto p-0"
onClick={() => navigate("/signup", { state: { from: location.pathname } })}
>
계정이 없으신가요?
</Button>
</div>
</AuthCard>
</>
);
junyeokk marked this conversation as resolved.
Show resolved Hide resolved
};
37 changes: 37 additions & 0 deletions client/src/components/auth/AuthSignUpForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useLocation, useNavigate } from "react-router-dom";

import { AuthCard } from "@/components/auth/AuthCard.tsx";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

export const AuthSignUpForm = () => {
const navigate = useNavigate();
const location = useLocation();

return (
<>
<AuthCard title="회원가입" description="회원가입을 해주세요">
<form className="space-y-4">
<div className="space-y-2">
<Input type="email" placeholder="이메일을 입력하세요" required />
<Input type="password" placeholder="비밀번호를 입력하세요" required />
<Input type="text" placeholder="이름을 입력해주세요" required />
<Input type="text" placeholder="닉네임을 입력해주세요" required />
</div>
<Button className="w-full" type="submit">
회원가입
</Button>
</form>
<div className="mt-4">
<Button
variant="link"
className="text-muted-foreground underline underline-offset-4 h-auto p-0"
onClick={() => navigate("/signin", { state: { from: location.pathname } })}
>
이미 계정이 있으신가요?
</Button>
</div>
</AuthCard>
</>
);
};
51 changes: 51 additions & 0 deletions client/src/components/auth/AuthSocialLoginButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { GitHub } from "@/components/icons/social/GitHub.tsx";
import { Google } from "@/components/icons/social/Google.tsx";
import { Kakao } from "@/components/icons/social/Kakao.tsx";
import { Naver } from "@/components/icons/social/Naver.tsx";
import { Button } from "@/components/ui/button.tsx";

import { useCustomToast } from "@/hooks/common/useCustomToast.ts";

export const AuthSocialLoginButtons = () => {
const { toast } = useCustomToast();

const handleSocialLogin = () => {
toast({
title: "서비스 준비 중",
description: "서비스가 현재 개발 중입니다. 곧 만나요!",
duration: 3000,
});
};
junyeokk marked this conversation as resolved.
Show resolved Hide resolved

return (
<>
<div className="relative my-4">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">Or continue with</span>
</div>
</div>

<div className="grid gap-2">
<Button variant="outline" className="w-full" onClick={handleSocialLogin}>
<GitHub />
<span className="text-muted-foreground">Github로 계속하기</span>
</Button>
<Button variant="outline" className="w-full" onClick={handleSocialLogin}>
<Google />
<span className="text-muted-foreground">Google로 계속하기</span>
</Button>
<Button variant="outline" className="w-full" onClick={handleSocialLogin}>
<Naver className="text-[#03C75A]" />
<span className="text-muted-foreground">네이버로 계속하기</span>
</Button>
<Button variant="outline" className="w-full" onClick={handleSocialLogin}>
<Kakao className="text-[#FEE500]" />
<span className="text-muted-foreground">카카오로 계속하기</span>
</Button>
</div>
</>
);
};
12 changes: 12 additions & 0 deletions client/src/components/icons/social/GitHub.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as React from "react";

export function GitHub(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.84 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0 1 12 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z"
/>
</svg>
);
}
24 changes: 24 additions & 0 deletions client/src/components/icons/social/Google.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react";

export function Google(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" {...props}>
<path
fill="#4285F4"
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="#34A853"
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="#FBBC05"
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="#EA4335"
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>
);
}
12 changes: 12 additions & 0 deletions client/src/components/icons/social/Kakao.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as React from "react";

export function Kakao(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 208 208" {...props}>
<path
fill="currentColor"
d="M104 0C46.56 0 0 36.71 0 82c0 29.28 19.47 54.89 48.75 69.22l-12.36 44.74c-0.76 2.77 2.4 5.12 5 3.68L90 166.52c4.6 0.63 9.31 0.95 14 0.95 57.44 0 104-36.71 104-82S161.44 0 104 0z"
/>
</svg>
);
}
13 changes: 13 additions & 0 deletions client/src/components/icons/social/Naver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from "react";

export function Naver(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M16.273 12.845 7.376 0H0v24h7.726V11.155L16.624 24H24V0h-7.727z"
transform="scale(0.8) translate(3.6, 3.6)"
/>
</svg>
);
}
12 changes: 3 additions & 9 deletions client/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,18 @@ import DesktopNavigation from "@/components/layout/navigation/DesktopNavigation"
import MobileNavigation from "@/components/layout/navigation/MobileNavigation";
import SearchModal from "@/components/search/SearchModal";

import { useCustomToast } from "@/hooks/common/useCustomToast.ts";
import { useKeyboardShortcut } from "@/hooks/common/useKeyboardShortcut";

import { TOAST_MESSAGES } from "@/constants/messages";

import { useMediaStore } from "@/store/useMediaStore";

export default function Header() {
const [modals, setModals] = useState({ search: false, rss: false, login: false, chat: false });
const { toast } = useCustomToast();
const isMobile = useMediaStore((state) => state.isMobile);
const toggleModal = (modalType: "search" | "rss" | "login" | "chat") => {
if (modalType === "login") {
toast(TOAST_MESSAGES.SERVICE_NOT_PREPARED);
return;
}

const toggleModal = (modalType: "search" | "rss" | "chat") => {
setModals((prev) => ({ ...prev, [modalType]: !prev[modalType] }));
};

useKeyboardShortcut("k", () => toggleModal("search"), true);

return (
Expand Down
12 changes: 9 additions & 3 deletions client/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,29 @@ import { useTapStore } from "@/store/useTapStore";

type SideBarType = {
handleRssModal: () => void;
handleLoginModal: () => void;
handleSidebar: () => void;
};

export default function SideBar({ handleRssModal, handleLoginModal, handleSidebar }: SideBarType) {
export default function SideBar({ handleRssModal, handleSidebar }: SideBarType) {
const navigate = useNavigate();
const { tap, setTap } = useTapStore();

const actionAndClose = (fn: () => void) => {
fn();
handleSidebar();
};

const handleSignIn = () => {
navigate("/signin");
handleSidebar();
};

return (
<div className="flex flex-col gap-4 p-4">
<Button onClick={() => navigate("/about")} variant="outline">
서비스 소개
</Button>
<Button variant="outline" className="w-full" onClick={() => actionAndClose(handleLoginModal)}>
<Button variant="outline" className="w-full" onClick={handleSignIn}>
로그인
</Button>
{tap === "main" ? (
Expand Down
Loading
Loading