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

- 로그인 요청 후 응답을 AuthData 로 저장 로직 구현
- Access/Refresh 토큰 구현 중
This commit is contained in:
geonhee-min
2025-12-01 16:22:40 +09:00
parent 877bbc582e
commit 45dc4cbaaa
11 changed files with 97 additions and 34 deletions

View File

@@ -3,7 +3,7 @@ import SignUpPage from './ui/page/signup/SignUpPage';
import Layout from './layouts/Layout';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { useAuthStore } from './store/authStore';
import { PageRouting } from './data/RoutingData';
import { PageRouting } from './const/PageRouting';
import LoginPage from './ui/page/login/LoginPage';
import ResetPasswordPage from './ui/page/resetPassword/ResetPasswordPage';
@@ -17,7 +17,7 @@ function App() {
<Route element={<LoginPage />} path={PageRouting["LOGIN"].path} />
<Route element={<SignUpPage />} path={PageRouting["SIGN_UP"].path} />
<Route element={<ResetPasswordPage />} path={PageRouting["RESET_PASSWORD"].path} />
{!(authData?.isLogedIn) ? <Route element={<Navigate to={PageRouting["LOGIN"].path} />} path="*" /> : null}
{!authData ? <Route element={<Navigate to={PageRouting["LOGIN"].path} />} path="*" /> : null}
</Route>
</Routes>
</Router>

View File

@@ -0,0 +1,9 @@
export const HttpResponse = {
"ACCESS_TOKEN_EXPIRED": "ACCESS TOKEN EXPIRED",
"REFRESH_TOKEN_EXPIRED": "REFRESH TOKEN EXPIRED",
"UNAUTHORIZED": "UNAUTHORIZED",
"OK": "OK",
"CREATED": "CREATED",
"BAD_REQUEST": "BAD REQUEST",
"INTERNAL_SERVER_ERROR": "INTERNAL SERVER ERROR"
} as const;

View File

@@ -16,5 +16,5 @@ export const PageRouting: Record<string, PageRoutingInfo> = {
USER_FOLLOWING: { path: "/info/following", title: "팔로잉 목록" },
USER_FOLLOWER: { path: "/info/follower", title: "팔로워 목록" },
SETTINGS: { path: "/settings", title: "설정" },
NOT_FOUD: { path: "/not-found", title: "존재하지 않는 페이지" },
NOT_FOUND: { path: "/not-found", title: "존재하지 않는 페이지" },
} as const;

View File

@@ -1,5 +1,4 @@
export type AuthData = {
accessToken: string;
refreshToken: string;
isLogedIn: boolean;
}

View File

@@ -20,22 +20,51 @@ export class AccountNetwork extends BaseNetwork {
async checkDuplication(data: CheckDuplicationRequest) {
const { type, value } = data;
return await this.instance.get<CheckDuplicationResponse>(`${this.baseUrl}/check-duplication?type=${type}&value=${value}`);
return await this.get<CheckDuplicationResponse>(
`${this.baseUrl}/check-duplication?type=${type}&value=${value}`
, {
authPass: true
}
);
}
async sendVerificationCode(data: SendVerificationCodeRequest) {
return await this.instance.post<SendVerificationCodeResponse>(this.baseUrl + "/send-verification-code", data);
return await this.post<SendVerificationCodeResponse>(
this.baseUrl + "/send-verification-code"
, data
, {
authPass: true
}
);
}
async verifyCode(data: VerifyCodeRequest) {
return await this.instance.post<VerifyCodeResponse>(this.baseUrl + "/verify-code", data);
return await this.post<VerifyCodeResponse>(
this.baseUrl + "/verify-code"
, data
, {
authPass: true
}
);
}
async signup(data: SignupRequest) {
return await this.instance.post<SignupResponse>(this.baseUrl + "/signup", data);
return await this.post<SignupResponse>(
this.baseUrl + "/signup"
, data
, {
authPass: true
}
);
}
async login(data: LoginRequest) {
return await this.instance.post<LoginResponse>(this.baseUrl + "/login", data);
return await this.post<LoginResponse>(
this.baseUrl + "/login"
, data
, {
authPass: true
}
);
}
}

View File

@@ -4,6 +4,7 @@ import type {
AxiosRequestConfig,
AxiosError,
AxiosResponse,
InternalAxiosRequestConfig,
} from "axios";
export class BaseNetwork {
@@ -29,11 +30,14 @@ export class BaseNetwork {
// ★ 요청 인터셉터
this.instance.interceptors.request.use(
(config) => {
// 예: 자동 토큰 추가
// const token = localStorage.getItem("token");
// if (token) {
// config.headers.Authorization = `Bearer ${token}`;
// }
const reqConfig = config as InternalAxiosRequestConfig & { authPass?: boolean };
if (reqConfig.authPass) {
return config;
}
const accessToken = localStorage.getItem("accessToken");
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
@@ -61,11 +65,11 @@ export class BaseNetwork {
/**
* 기본 CRUD 메서드
*/
protected async get<T = any>(url: string, config?: AxiosRequestConfig) {
protected async get<T = any>(url: string, config?: AxiosRequestConfig & { authPass?: boolean }) {
return await this.instance.get<T>(url, config);
}
protected async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
protected async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig & { authPass?: boolean }) {
return await this.instance.post<T>(url, data, config);
}
}

View File

@@ -8,6 +8,15 @@ interface AuthStoreProps {
export const useAuthStore = create<AuthStoreProps>((set) => ({
authData: undefined,
login: (data: AuthData) => set({ authData: data }),
logout: () => set({ authData: undefined })
login: (data: AuthData) => {
set({ authData: data });
Object.entries(data)
.forEach((entry) => {
localStorage.setItem(entry[0], entry[1]);
})
},
logout: () => {
set({ authData: undefined });
localStorage.clear();
}
}));

View File

@@ -7,17 +7,18 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useCallback, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { PageRouting } from '@/data/RoutingData';
import { PageRouting } from '@/const/PageRouting';
import * as z from 'zod';
import { Separator } from '@/components/ui/separator';
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';
import { useAuthStore } from '@/store/authStore';
export default function LoginPage() {
const [isLoading, setIsLoading] = useState<boolean>(false);
const { login } = useAuthStore();
const navigate = useNavigate();
const accountNetwork = new AccountNetwork();
const loginForm = useForm<z.infer<typeof LoginSchema>>({
@@ -37,17 +38,19 @@ export default function LoginPage() {
navigate(PageRouting["RESET_PASSWORD"].path);
}, []);
const moveToMainPage = useCallback(() => {
const moveToHomePage = useCallback(() => {
navigate(PageRouting["HOME"].path);
}, []);
// TODO 33 로그인 기능 구현
const login = async () => {
const reqLogin = async () => {
if (isLoading) return;
const type = Validator.isEmail(id) ? 'email' : 'accountId';
const data: LoginRequest = new LoginRequest(type, id, password);
setIsLoading(true);
const loginPromise = accountNetwork.login(data);
toast.promise<{ message?: string }>(
@@ -55,9 +58,9 @@ export default function LoginPage() {
try {
loginPromise.then((res) => {
if (res.data.success) {
resolve({message: ''})
resolve({message: ''});
} else {
reject(res.data.message)
reject(res.data.message);
}
})
} catch (err) {
@@ -71,11 +74,18 @@ export default function LoginPage() {
}
);
// loginPromise.then((res) => {
// if (res.data.success) {
// moveToMainPage();
// }
// });
loginPromise
.then((res) => {
if (res.data.success) {
const data = {
accessToken: res.data.accessToken!,
refreshToken: res.data.refreshToken!
}
login({ ...data });
moveToHomePage();
}
})
.finally(() => setIsLoading(false));
}
const TextSeparator = ({ text }: { text: string }) => {
@@ -148,7 +158,7 @@ export default function LoginPage() {
className="w-full bg-indigo-500 hover:bg-indigo-400"
type="button"
disabled={id.trim().length < 1 || password.trim().length < 1}
onClick={login}
onClick={reqLogin}
>
</Button>

View File

@@ -7,7 +7,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useState, useCallback } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { PageRouting } from '@/data/RoutingData';
import { PageRouting } from '@/const/PageRouting';
import * as z from 'zod';
export default function ResetPasswordPage() {

View File

@@ -17,7 +17,7 @@ import { CheckDuplicationRequest, SignupRequest } from '@/data/request';
import { AccountNetwork } from '@/network/AccountNetwork';
import { toast } from 'sonner';
import { useNavigate } from 'react-router-dom';
import { PageRouting } from '@/data/RoutingData';
import { PageRouting } from '@/const/PageRouting';
export default function SignUpPage() {
const [emailVerificationModalOpen, setEmailVerificationModalOpen] = useState<boolean>(false);

View File

@@ -4,6 +4,9 @@ import tailwindcss from '@tailwindcss/vite'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
server: {
port: 5185
},
plugins: [
react(),
tailwindcss()