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 Layout from './layouts/Layout';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { useAuthStore } from './store/authStore'; import { useAuthStore } from './store/authStore';
import { PageRouting } from './data/RoutingData'; import { PageRouting } from './const/PageRouting';
import LoginPage from './ui/page/login/LoginPage'; import LoginPage from './ui/page/login/LoginPage';
import ResetPasswordPage from './ui/page/resetPassword/ResetPasswordPage'; import ResetPasswordPage from './ui/page/resetPassword/ResetPasswordPage';
@@ -17,7 +17,7 @@ function App() {
<Route element={<LoginPage />} path={PageRouting["LOGIN"].path} /> <Route element={<LoginPage />} path={PageRouting["LOGIN"].path} />
<Route element={<SignUpPage />} path={PageRouting["SIGN_UP"].path} /> <Route element={<SignUpPage />} path={PageRouting["SIGN_UP"].path} />
<Route element={<ResetPasswordPage />} path={PageRouting["RESET_PASSWORD"].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> </Route>
</Routes> </Routes>
</Router> </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_FOLLOWING: { path: "/info/following", title: "팔로잉 목록" },
USER_FOLLOWER: { path: "/info/follower", title: "팔로워 목록" }, USER_FOLLOWER: { path: "/info/follower", title: "팔로워 목록" },
SETTINGS: { path: "/settings", title: "설정" }, SETTINGS: { path: "/settings", title: "설정" },
NOT_FOUD: { path: "/not-found", title: "존재하지 않는 페이지" }, NOT_FOUND: { path: "/not-found", title: "존재하지 않는 페이지" },
} as const; } as const;

View File

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

View File

@@ -20,22 +20,51 @@ export class AccountNetwork extends BaseNetwork {
async checkDuplication(data: CheckDuplicationRequest) { async checkDuplication(data: CheckDuplicationRequest) {
const { type, value } = data; 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) { 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) { 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) { 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) { 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, AxiosRequestConfig,
AxiosError, AxiosError,
AxiosResponse, AxiosResponse,
InternalAxiosRequestConfig,
} from "axios"; } from "axios";
export class BaseNetwork { export class BaseNetwork {
@@ -29,12 +30,15 @@ export class BaseNetwork {
// ★ 요청 인터셉터 // ★ 요청 인터셉터
this.instance.interceptors.request.use( this.instance.interceptors.request.use(
(config) => { (config) => {
// 예: 자동 토큰 추가 const reqConfig = config as InternalAxiosRequestConfig & { authPass?: boolean };
// const token = localStorage.getItem("token"); if (reqConfig.authPass) {
// if (token) { return config;
// config.headers.Authorization = `Bearer ${token}`; }
// } const accessToken = localStorage.getItem("accessToken");
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config; return config;
}, },
(error: AxiosError) => Promise.reject(error) (error: AxiosError) => Promise.reject(error)
@@ -61,11 +65,11 @@ export class BaseNetwork {
/** /**
* 기본 CRUD 메서드 * 기본 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); 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); return await this.instance.post<T>(url, data, config);
} }
} }

View File

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

View File

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

View File

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

View File

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