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", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"local": "vite --mode local",
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"lint": "eslint .", "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 './App.css';
import SignUpPage from './ui/page/signup/SignUpPage'; import SignUpPage from './ui/page/signup/SignUpPage';
import Layout from './layouts/Layout'; import Layout from './layouts/Layout';

View File

@@ -2,4 +2,6 @@ import { BaseResponse } from "../BaseResponse";
export class LoginResponse extends BaseResponse { export class LoginResponse extends BaseResponse {
success!: boolean; 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 { SidebarProvider } from "@/components/ui/sidebar";
import Header from "@/ui/component/Header"; import Header from "@/ui/component/Header";
import { useAuthStore } from '@/store/authStore'; import { useAuthStore } from '@/store/authStore';
import { Toaster, type ToasterProps } from "sonner";
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react";
export default function Layout() { export default function Layout() {
const { authData } = useAuthStore(); const { authData } = useAuthStore();
return ( return (
<SidebarProvider <>
defaultOpen={false} <Toaster
id="root" position="top-center"
> icons={{
<SideBar /> success: <CircleCheckIcon className="size-4" fill="#15b815" color="white" />,
<div className="flex flex-col w-full h-full"> error: <OctagonXIcon className="size-4" fill="#f14e4e" color="white" />,
{ authData?.isLogedIn ? <Header /> : null} info: <InfoIcon className="size-4" fill="black" color="white" />,
{/* <Header /> */} warning: <TriangleAlertIcon className="size-4" fill="#ffd500" color="white" />,
<Outlet /> loading: <Loader2Icon className="size-4 animate-spin" fill="white" color="black" />
</div> }}
</SidebarProvider> />
<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' import App from './App.tsx'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> // <StrictMode>
<App /> <App />
</StrictMode>, // </StrictMode>,
) )

View File

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

View File

@@ -1,5 +1,7 @@
export class Validator { export class Validator {
static isEmail = (value: string): boolean => { static isEmail = (value: any) => {
return /^[^\s@]+@[^\s@]+\.[*\s@]+$/.test(value); 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);
};
} }