issue #33
All checks were successful
Test CI / build (push) Successful in 15s

- 로그인 버튼 비활성화 오류 해결
- 로그인 요칭 및 응답에 따른 토스트 구현
This commit is contained in:
2025-11-30 18:20:47 +09:00
parent 49cda54644
commit 877bbc582e
10 changed files with 72 additions and 38 deletions

View File

@@ -4,6 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"local": "vite --mode local",
"dev": "vite",
"build": "vite build",
"lint": "eslint .",

View File

@@ -1,6 +1,3 @@
import { useState } from 'react';
import reactLogo from './assets/react.svg';
import viteLogo from '/vite.svg';
import './App.css';
import SignUpPage from './ui/page/signup/SignUpPage';
import Layout from './layouts/Layout';

View File

@@ -2,4 +2,6 @@ import { BaseResponse } from "../BaseResponse";
export class LoginResponse extends BaseResponse {
success!: boolean;
accessToken?: string;
refreshToken?: string;
}

1
src/hooks/use-toast.ts Normal file
View File

@@ -0,0 +1 @@
import { toast } from 'sonner';

View File

@@ -3,21 +3,40 @@ import { Outlet } from "react-router-dom";
import { SidebarProvider } from "@/components/ui/sidebar";
import Header from "@/ui/component/Header";
import { useAuthStore } from '@/store/authStore';
import { Toaster, type ToasterProps } from "sonner";
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react";
export default function Layout() {
const { authData } = useAuthStore();
return (
<SidebarProvider
defaultOpen={false}
id="root"
>
<SideBar />
<div className="flex flex-col w-full h-full">
{ authData?.isLogedIn ? <Header /> : null}
{/* <Header /> */}
<Outlet />
</div>
</SidebarProvider>
<>
<Toaster
position="top-center"
icons={{
success: <CircleCheckIcon className="size-4" fill="#15b815" color="white" />,
error: <OctagonXIcon className="size-4" fill="#f14e4e" color="white" />,
info: <InfoIcon className="size-4" fill="black" color="white" />,
warning: <TriangleAlertIcon className="size-4" fill="#ffd500" color="white" />,
loading: <Loader2Icon className="size-4 animate-spin" fill="white" color="black" />
}}
/>
<SidebarProvider
defaultOpen={false}
id="root"
>
<SideBar />
<div className="flex flex-col w-full h-full">
{ authData?.isLogedIn ? <Header /> : null}
{/* <Header /> */}
<Outlet />
</div>
</SidebarProvider>
</>
);
}

View File

@@ -4,7 +4,7 @@ import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
// <StrictMode>
<App />
</StrictMode>,
// </StrictMode>,
)

View File

@@ -14,6 +14,7 @@ import { Validator } from '@/util/Validator';
import { LoginRequest } from '@/data/request/account/LoginRequest';
import { AccountNetwork } from '@/network/AccountNetwork';
import { toast } from 'sonner';
import { LoginResponse } from '@/data/response';
export default function LoginPage() {
const [isLoading, setIsLoading] = useState<boolean>(false);
@@ -26,6 +27,7 @@ export default function LoginPage() {
password: ""
}
});
const { id, password } = { id: loginForm.watch('id'), password: loginForm.watch('password') };
const moveToSignUpPage = useCallback(() => {
navigate(PageRouting["SIGN_UP"].path);
@@ -42,28 +44,38 @@ export default function LoginPage() {
// TODO 33 로그인 기능 구현
const login = async () => {
if (isLoading) return;
const { id, password } = loginForm.getValues();
const type = Validator.isEmail(id) ? 'email' : 'accountId';
const data: LoginRequest = new LoginRequest(type, id, password);
const loginPromise = accountNetwork.login(data);
toast.promise(
loginPromise,
toast.promise<{ message?: string }>(
() => new Promise(async (resolve, reject) => {
try {
loginPromise.then((res) => {
if (res.data.success) {
resolve({message: ''})
} else {
reject(res.data.message)
}
})
} catch (err) {
reject ("서버 에러 발생");
}
}),
{
loading: "로그인 중입니다.",
success: (res) => res.data.success ? "로그인이 완료되었습니다." : res.data.message,
error: "로그인에 실패하였습니다."
success: "로그인이 완료되었습니다.",
error: (err) => `${err}`
}
);
loginPromise.then((res) => {
if (res.data.success) {
moveToMainPage();
}
});
// loginPromise.then((res) => {
// if (res.data.success) {
// moveToMainPage();
// }
// });
}
const TextSeparator = ({ text }: { text: string }) => {
@@ -89,7 +101,7 @@ export default function LoginPage() {
control={loginForm.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-login-id"></FieldLabel>
<FieldLabel htmlFor="form-login-id"> </FieldLabel>
<Input
{...field}
type="text"
@@ -111,6 +123,7 @@ export default function LoginPage() {
<Button
className="p-0 bg-transparent hover:bg-transparent h-fit w-fit text-xs text-gray-400 hover:text-gray-500 cursor-pointer"
onClick={moveToResetPasswordPage}
type="button"
>
?
</Button>
@@ -133,17 +146,16 @@ export default function LoginPage() {
>
<Button
className="w-full bg-indigo-500 hover:bg-indigo-400"
type="submit"
form="form-login"
disabled={
(loginForm.getValues("id").trim.length < 1)
&& (loginForm.getValues("password").trim.length < 1)
}>
type="button"
disabled={id.trim().length < 1 || password.trim().length < 1}
onClick={login}
>
</Button>
<TextSeparator text="또는" />
<Button
className="w-full text-violet-500 bg-white border border-violet-500 hover:bg-violet-500 hover:text-white"
type="button"
onClick={moveToSignUpPage}
>

View File

@@ -1,5 +1,7 @@
export class Validator {
static isEmail = (value: string): boolean => {
return /^[^\s@]+@[^\s@]+\.[*\s@]+$/.test(value);
}
static isEmail = (value: any) => {
if (typeof value !== 'string') return false;
const email = value.trim();
return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(email);
};
}