Commit eab79372 authored by Jiwon Yoon's avatar Jiwon Yoon
Browse files

Merge branch 'main' into Yoon

parents 94aeb983 4ff854ff
import React from "react"; import React from "react";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import { AuthProvider } from "./auth/auth.context";
import { Header } from "./commons"; import { Header } from "./commons";
const App = () => ( const App = () => (
<div> <AuthProvider>
<Header /> <Header />
<Outlet /> <Outlet />
</div> </AuthProvider>
); );
export default App; export default App;
import axios from "axios";
import baseUrl from "./baseUrl";
export const login = async (email: string, password: string) => {
const { data } = await axios.post(`${baseUrl}/auth/login`, {
email,
password,
});
return data;
};
export const logout = async () => {
const { data } = await axios.get(`${baseUrl}/auth/logout`);
return data;
};
export default "/api";
import axios from "axios";
export * as authApi from "./auth.api";
import React, { useState } from "react"; import React, { ChangeEvent, FormEvent, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { catchErrors } from "../helpers";
// type LoginProps = { import { SpinnerIcon } from "../icons";
import { useAuth } from "./auth.context";
// };
export const Login = () => { export const Login = () => {
// interface IUSER { const [error, setError] = useState("");
// email: string; const [loading, setLoading] = useState(false);
// password: string; const [loginData, setLoginData] = useState({ email: "", password: "" });
// } const navigate = useNavigate();
const { login } = useAuth();
// const [error, setError] = useState("");
// const [disabled, setDisabled] = useState(false);
// const [success, setSuccess] = useState(false);
// const navigate = useNavigate();
// function handleChange(event: type) {}
// function handleSubmit(params: type) {} function handleChange(e: ChangeEvent<HTMLInputElement>) {
const { name, value } = e.currentTarget;
setLoginData({ ...loginData, [name]: value });
}
// if (success) { async function handleSubmit(e: FormEvent) {
// alert("회원가입 되었습니다"); e.preventDefault();
// navigate(`../`); console.log(loginData);
// } const { email, password } = loginData;
try {
setLoading(true);
await login(email, password, () => navigate("/", { replace: true }));
} catch (error) {
setLoading(false);
catchErrors(error, setError);
}
}
return ( return (
<div className="flex justify-center mt-3"> <div className="flex justify-center mt-3">
<div className="flex flex-col space-y-4 mt-5 text-xl font-bold"> <form
onSubmit={handleSubmit}
className="flex flex-col space-y-4 mt-5 text-xl font-bold"
>
<label className="block text-gray-700 text-sm font-bold mb-2 mt-3"> <label className="block text-gray-700 text-sm font-bold mb-2 mt-3">
이메일 이메일
</label> </label>
<input <input
onChange={handleChange}
className="shadow appearance-none border rounded py-2 px-3 text-gray-70" className="shadow appearance-none border rounded py-2 px-3 text-gray-70"
id="email" name="email"
type="email" type="email"
autoComplete="username"
placeholder="이메일을 입력하세요" placeholder="이메일을 입력하세요"
value={loginData.email}
/> />
<label className="block text-gray-700 text-sm font-bold mb-2 mt-3"> <label className="block text-gray-700 text-sm font-bold mb-2 mt-3">
비밀번호 비밀번호
</label> </label>
<input <input
onChange={handleChange}
className="shadow appearance-none border rounded py-2 px-3 text-gray-70" className="shadow appearance-none border rounded py-2 px-3 text-gray-70"
id="username" name="password"
type="password" type="password"
autoComplete="current-password"
placeholder="비밀번호를 입력하세요" placeholder="비밀번호를 입력하세요"
value={loginData.password}
/> />
{error && (
<div className="text-red-500 text-sm mb-6">
<p>{error}</p>
</div>
)}
<div className="text-center"> <div className="text-center">
<button className="bg-themeColor text-white border rounded w-100 py-2 px-3 mt-3"> <button
type="submit"
disabled={loading ? true : false}
className="bg-themeColor text-white border rounded w-100 py-2 px-3 mt-3"
>
{loading && (
<SpinnerIcon className="animate-spin h-5 w-5 mr-1 text-white" />
)}
로그인 로그인
</button> </button>
</div> </div>
</div> </form>
</div> </div>
); );
}; };
import React, {
createContext,
FC,
ReactNode,
useContext,
useState,
} from "react";
import { IUser } from "../types";
import { getLocalUser, handleLogin, handleLogout } from "./auth.helper";
interface IAuthContext {
login: (email: string, password: string, cb?: VoidFunction) => Promise<void>;
logout: (cb?: VoidFunction) => Promise<void>;
user: IUser;
}
const AuthContext = createContext<IAuthContext>({
login: async () => {},
logout: async () => {},
user: { isLoggedIn: false },
});
export const AuthProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState(getLocalUser());
const login = async (
email: string,
password: string,
cb: VoidFunction = () => {}
) => {
const user = await handleLogin(email, password);
setUser(user);
cb();
};
const logout = async (cb: VoidFunction = () => {}) => {
await handleLogout();
setUser({ ...user, isLoggedIn: false });
cb();
};
return (
<AuthContext.Provider value={{ login, logout, user }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
import { authApi } from "../apis";
import { IUser } from "../types";
const LOCAL_USER_INFO = "survey-user-info";
/**
* 1. 백엔드 로그인을 호출하여 로그인 정보를 얻습니다.
* 2. 로컬 저장소에 저장합니다.
* 3. 사용자 정보를 반환합니다.
* @param email 이메일
* @param password 비밀번호
* @returns 사용자 정보
*/
export const handleLogin = async (email: string, password: string) => {
const user: IUser = await authApi.login(email, password);
// 로컬 저장소에는 로그인 여부만 저장
localStorage.setItem(
LOCAL_USER_INFO,
JSON.stringify({
isLoggedIn: user.isLoggedIn,
})
);
return user;
};
/**
* 로컬 저장소의 정보를 삭제합니다.
* 백엔드 로그아웃을 호출하여 쿠키를 제거합니다.
*/
export const handleLogout = async () => {
console.log("handle logout called");
localStorage.removeItem(LOCAL_USER_INFO);
try {
await authApi.logout();
} catch (error) {
console.log("logout 중에 에러 발생:", error);
}
};
/**
* 1. 로컬 저장소에 저장된 사용자 로그인 정보를 반환합니다.
* 2. 로컬 저장소에 정보가 없으면 { isLoggedIn: false }를 반환합니다.
* @returns 로컬 저장소에 저장된 사용자 정보
*/
export const getLocalUser = () => {
console.log("get local user called");
const userInfo = localStorage.getItem(LOCAL_USER_INFO);
const user: IUser = { isLoggedIn: false };
if (!userInfo) {
return user;
}
const userData = JSON.parse(userInfo);
if (userData.isLoggedIn) {
user.isLoggedIn = true;
} else {
user.isLoggedIn = false;
}
return user;
};
import React from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useAuth } from "../auth/auth.context";
export const Header = () => ( export const Header = () => {
<div className="bg-white border-b-2 border-b-themeColor px-2 sm:px-4 py-2.5"> const { user, logout } = useAuth();
<div className="container flex flex-wrap justify-between items-center mx-auto">
<Link to="/" className="font-bold text-2xl text-themeColor"> return (
Simple Survey Form <div className="bg-white border-b-2 border-b-themeColor px-2 sm:px-4 py-2.5">
</Link> <div className="container flex flex-wrap justify-between items-center mx-auto">
<div className="md:flex items-center justify-end md:flex-1 lg:w-0"> <Link to="/" className="font-bold text-2xl text-themeColor">
<Link Simple Survey Form
to="/login"
className="whitespace-nowrap font-bold text-gray-600 hover:text-themeColor mx-1 py-2 px-3 rounded-md"
>
Login
</Link>
<Link
to="/signup"
className="whitespace-nowrap font-bold text-white hover:bg-blue-500 mx-1 py-2 px-3 bg-themeColor rounded-md "
>
Sign Up
</Link> </Link>
<div className="md:flex items-center justify-end md:flex-1 lg:w-0">
{user.isLoggedIn ? (
<button onClick={() => logout()}>Logout</button>
) : (
<Link
to="/login"
className="whitespace-nowrap font-bold text-gray-600 hover:text-themeColor mx-1 py-2 px-3 rounded-md"
>
Login
</Link>
)}
<Link
to="/signup"
className="whitespace-nowrap font-bold text-white hover:bg-blue-500 mx-1 py-2 px-3 bg-themeColor rounded-md "
>
Sign Up
</Link>
</div>
</div> </div>
</div> </div>
</div> );
); };
export const catchErrors = (error: any, dispalyError: Function) => {
let errorMsg;
if (error.response) {
errorMsg = error.response.data;
console.log("Error response:", errorMsg);
} else if (error.request) {
errorMsg = error.request;
console.log("Error request:", errorMsg);
} else {
errorMsg = error.message;
console.log("Error message:", errorMsg);
}
dispalyError(errorMsg);
};
export { catchErrors } from "./catchErrors";
import React, { FC, ReactNode } from "react";
export const SpinnerIcon = ({ ...props }) => {
return (
// <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" {...props}>
// <path d="M222.7 32.15C227.7 49.08 218.1 66.9 201.1 71.94C121.8 95.55 64 169.1 64 255.1C64 362 149.1 447.1 256 447.1C362 447.1 448 362 448 255.1C448 169.1 390.2 95.55 310.9 71.94C293.9 66.9 284.3 49.08 289.3 32.15C294.4 15.21 312.2 5.562 329.1 10.6C434.9 42.07 512 139.1 512 255.1C512 397.4 397.4 511.1 256 511.1C114.6 511.1 0 397.4 0 255.1C0 139.1 77.15 42.07 182.9 10.6C199.8 5.562 217.6 15.21 222.7 32.15V32.15z" />
// </svg>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth={4}
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
);
};
export { SpinnerIcon } from "./SpinnerIcon";
export interface IUser {
email?: string;
isLoggedIn: boolean;
_id?: string;
}
...@@ -6,5 +6,14 @@ const common = require("./webpack.common.js"); ...@@ -6,5 +6,14 @@ const common = require("./webpack.common.js");
module.exports = merge(common, { module.exports = merge(common, {
mode: "development", mode: "development",
devtool: "eval-cheap-module-source-map", devtool: "eval-cheap-module-source-map",
devServer: { historyApiFallback: true }, devServer: {
proxy: [
{
context: ["/api"],
target: "http://localhost:3000",
changeOrigin: true,
},
],
historyApiFallback: true,
},
}); });
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment