Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…into Feature/#156_server에_Page별_EditorCRDT_적용

# Conflicts:
#	client/src/App.tsx
#	client/src/features/editor/Editor.tsx
#	client/src/features/editor/components/block/Block.tsx
#	client/tsconfig.tsbuildinfo
  • Loading branch information
pipisebastian committed Nov 21, 2024
2 parents 71db95e + 6a3fe0e commit bcddda1
Show file tree
Hide file tree
Showing 45 changed files with 1,238 additions and 242 deletions.
16 changes: 16 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import { useEffect } from "react";
import { useRefreshQuery } from "@apis/auth";
import { ErrorModal } from "@components/modal/ErrorModal";
import { WorkSpace } from "@features/workSpace/WorkSpace";
import { useErrorStore } from "@stores/useErrorStore";
import { useUserInfo } from "@stores/useUserStore";
import { useSocketStore } from "./stores/useSocketStore";

const App = () => {
// TODO 라우터, react query 설정
const { id, name, accessToken } = useUserInfo();
const { refetch: refreshToken } = useRefreshQuery();
const { isErrorModalOpen, errorMessage } = useErrorStore();

useEffect(() => {
if (id && name && !accessToken) {
refreshToken();
}
}, []);

useEffect(() => {
const socketStore = useSocketStore.getState();
socketStore.init();
Expand All @@ -13,8 +27,10 @@ const App = () => {
}, 0);
};
}, []);

return (
<>
{isErrorModalOpen && <ErrorModal errorMessage={errorMessage} />}
<WorkSpace />
</>
);
Expand Down
83 changes: 59 additions & 24 deletions client/src/apis/auth.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,70 @@
import { useMutation } from "@tanstack/react-query";
import axios from "axios";

export const useSignupMutation = () => {
const fetcher = ({
name,
email,
password,
}: {
name: string;
email: string;
password: string;
}) => {
return axios.post("/auth/register", { name, email, password });
};
import { useMutation, useQuery } from "@tanstack/react-query";
import { useUserActions } from "@stores/useUserStore";
import { unAuthorizationFetch, fetch } from "./axios";

const authKey = {
all: ["auth"] as const,
refresh: () => [...authKey.all, "refresh"] as const,
};

export const useSignupMutation = (onSuccess: () => void) => {
const fetcher = ({ name, email, password }: { name: string; email: string; password: string }) =>
unAuthorizationFetch.post("/auth/register", { name, email, password });

return useMutation({
mutationFn: fetcher,
onSuccess: () => {
onSuccess();
},
});
};

export const useLoginMutation = (onSuccess: () => void) => {
const { setUserInfo } = useUserActions();

const fetcher = ({ email, password }: { email: string; password: string }) =>
unAuthorizationFetch.post("/auth/login", { email, password });

return useMutation({
mutationFn: fetcher,
onSuccess: (response) => {
const { id, name } = response.data;
const [, accessToken] = response.headers.authorization.split(" ");
setUserInfo(id, name, accessToken);
onSuccess();
},
});
};

export const useLoginMutation = () => {
const fetcher = ({ email, password }: { email: string; password: string }) => {
return axios.post("/auth/login", { email, password });
};
export const useLogoutMutation = (onSuccess: () => void) => {
const { removeUserInfo } = useUserActions();

const fetcher = () => fetch.post("/auth/logout");

return useMutation({
mutationFn: fetcher,
// TODO 성공했을 경우 accessToken 저장 (zustand? localStorage? cookie?)
// accessToken: cookie (쿠기 다 때려넣기...) / localStorage / zustand (번거로움..귀찮음.. 안해봤음..)
// refreshToken: cookie,
// onSuccess: (data) => {
// },
onSuccess: () => {
removeUserInfo();
onSuccess();
},
});
};

export const useRefreshQuery = () => {
const { updateAccessToken } = useUserActions();

const fetcher = () => fetch.get("/auth/refresh");

return useQuery({
queryKey: authKey.refresh(),
queryFn: async () => {
const response = await fetcher();

const [, accessToken] = response.headers.authorization.split(" ");
updateAccessToken(accessToken);

return response.data;
},
enabled: false,
});
};
102 changes: 102 additions & 0 deletions client/src/apis/axios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import axios, { AxiosError, CreateAxiosDefaults, InternalAxiosRequestConfig } from "axios";
import { useErrorStore } from "@stores/useErrorStore";
import { useUserStore } from "@stores/useUserStore";

interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
_retry?: boolean;
}

const baseConfig: CreateAxiosDefaults = {
baseURL: `${import.meta.env.VITE_API_URL}`,
withCredentials: true,
};

interface ApiError {
message: string;
code?: string;
}

const getErrorMessage = (error: AxiosError<ApiError>) => {
return error.response?.data?.message || error.message || "알 수 없는 오류가 발생했습니다.";
};

const handleGlobalError = (error: AxiosError<ApiError>) => {
const errorStore = useErrorStore.getState();
const errorMessage = getErrorMessage(error);

if (!error.response) {
errorStore.setErrorModal(true, "네트워크 연결을 확인해주세요.");
return;
}

// 에러 상태 코드에 따른 처리
switch (error.response.status) {
case 401:
// 이미 access token. refresh token이 만료된 경우 여기로 넘어옴.
useUserStore.getState().actions.removeUserInfo();
errorStore.setErrorModal(true, "유효하지 않은 사용자입니다. 다시 로그인해주세요.");
break;

case 500:
case 502:
case 503:
errorStore.setErrorModal(true, "서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
break;
default:
errorStore.setErrorModal(true, errorMessage);
}
};

export const unAuthorizationFetch = axios.create(baseConfig);

export const fetch = axios.create(baseConfig);

fetch.interceptors.request.use(
function (config) {
const { accessToken } = useUserStore.getState();

if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}

return config;
},
function (error) {
handleGlobalError(error);
return Promise.reject(error);
},
);

fetch.interceptors.response.use(
function (response) {
return response;
},
async function (error: AxiosError) {
const originalRequest: CustomAxiosRequestConfig | undefined = error.config;

if (error.response?.status === 401 && originalRequest && !originalRequest._retry) {
originalRequest._retry = true;

try {
// access token이 만료된 경우 refresh token으로 새로운 access token을 발급받음
// 이때 refresh token만 있기에 unAuthorizationFetch를 사용.
// 만약 fetch를 사용하면 401 에러가 무한으로 발생함.
const response = await unAuthorizationFetch.get("/auth/refresh");

const [, accessToken] = response.headers.authorization.split(" ");

useUserStore.setState({ accessToken });
originalRequest.headers.Authorization = `Bearer ${accessToken}`;

return fetch(originalRequest);
} catch (refreshError) {
handleGlobalError(refreshError as AxiosError<ApiError>);
return Promise.reject(refreshError);
}
}

handleGlobalError(error as AxiosError<ApiError>);
return Promise.reject(error);
},
);
``;
2 changes: 2 additions & 0 deletions client/src/components/modal/modal.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export const modalContent = css({
alignItems: "center",
width: "100%",
height: "100%",
paddingTop: "md",
paddingBottom: "md",
textAlign: "center",
});

Expand Down
11 changes: 11 additions & 0 deletions client/src/components/sidebar/MenuButton.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,14 @@ export const textBox = css({
textStyle: "display-medium20",
color: "gray.900",
});

export const menuDropdown = css({
zIndex: "dropdown",
position: "absolute",
top: "100%",
right: "0",
borderRadius: "md",
width: "100px",
marginTop: "sm",
boxShadow: "md",
});
10 changes: 4 additions & 6 deletions client/src/components/sidebar/MenuButton.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { useModal } from "@components/modal/useModal";
import { AuthModal } from "@src/features/auth/AuthModal";
import { useUserInfo } from "@stores/useUserStore";
import { menuItemWrapper, imageBox, textBox } from "./MenuButton.style";

export const MenuButton = () => {
const { isOpen, openModal, closeModal } = useModal();
const { name } = useUserInfo();

return (
<>
<button className={menuItemWrapper} onClick={openModal}>
<button className={menuItemWrapper}>
<div className={imageBox}></div>
<p className={textBox}>Noctturn</p>
<p className={textBox}>{name ?? "Nocta"}</p>
</button>
<AuthModal isOpen={isOpen} onClose={closeModal} />
</>
);
};
11 changes: 9 additions & 2 deletions client/src/components/sidebar/Sidebar.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,22 @@ export const navWrapper = css({
gap: "md",
flexDirection: "column",
width: "100%",
height: "calc(100% - 176px)",
overflowX: "hidden",
overflowY: "scroll",
});

export const plusIconBox = css({
display: "flex",
justifyContent: "start",
marginBlock: "10px",
position: "absolute",
bottom: "0px",
gap: "md",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
height: "60px",
paddingInline: "md",
justifyItems: "center",
});

export const sidebarToggleButton = css({
Expand Down
4 changes: 3 additions & 1 deletion client/src/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useIsSidebarOpen, useSidebarActions } from "@stores/useSidebarStore";
import { motion } from "framer-motion";
import { IconButton } from "@components/button/IconButton";
import { Modal } from "@components/modal/modal";
import { useModal } from "@components/modal/useModal";
import { MAX_VISIBLE_PAGE } from "@src/constants/page";
import { useIsSidebarOpen, useSidebarActions } from "@src/stores/useSidebarStore";
import { AuthButton } from "@src/features/auth/AuthButton";
import { Page } from "@src/types/page";
import { MenuButton } from "./MenuButton";
import { PageItem } from "./PageItem";
Expand Down Expand Up @@ -70,6 +71,7 @@ export const Sidebar = ({
</motion.nav>
<motion.div className={plusIconBox} variants={contentVariants}>
<IconButton icon="➕" onClick={handleAddPageButtonClick} size="sm" />
<AuthButton />
</motion.div>
<Modal isOpen={isOpen} primaryButtonLabel="확인" primaryButtonOnClick={closeModal}>
<p>
Expand Down
49 changes: 49 additions & 0 deletions client/src/features/auth/AuthButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useLogoutMutation } from "@apis/auth";
import { useCheckLogin } from "@stores/useUserStore";
import { TextButton } from "@components/button/textButton";
import { Modal } from "@components/modal/modal";
import { useModal } from "@components/modal/useModal";
import { AuthModal } from "./AuthModal";

export const AuthButton = () => {
const isLogin = useCheckLogin();

const {
isOpen: isAuthModalOpen,
openModal: openAuthModal,
closeModal: closeAuthModal,
} = useModal();

const {
isOpen: isLogoutModalOpen,
openModal: openLogoutModal,
closeModal: closeLogoutModal,
} = useModal();

const { mutate: logout } = useLogoutMutation(closeLogoutModal);

return (
<>
{isLogin ? (
<TextButton variant="secondary" onClick={openLogoutModal}>
로그아웃
</TextButton>
) : (
<TextButton variant="secondary" onClick={openAuthModal}>
로그인
</TextButton>
)}

<AuthModal isOpen={isAuthModalOpen} onClose={closeAuthModal} />
<Modal
isOpen={isLogoutModalOpen}
primaryButtonLabel="로그아웃"
primaryButtonOnClick={logout}
secondaryButtonLabel="취소"
secondaryButtonOnClick={closeLogoutModal}
>
<p>로그아웃 하시겠습니까?</p>
</Modal>
</>
);
};
Loading

0 comments on commit bcddda1

Please sign in to comment.