Skip to content

Commit

Permalink
feat: 토큰 만료 후 인증이 필요한 서비스에 접근시 자동 로그아웃 되는 기능을 구현한다 (#554)
Browse files Browse the repository at this point in the history
* refactor: 로그아웃 메서드 수정

- 멤버 토큰 삭제 기능 개선
- 멤버 정보 삭제 기능 추가

[#543]

* feat: fetch 추상화 유틸 구현

[#543]

* feat: 만료된 토큰에 대한 핸들링 메서드 구현

[#543]

* feat: 현재 가지고 있는 필터 정보를 요청 형식에 맞게 반환하는 메서드 구현

[#543]

* refactor: msw에 면경된 API 명세 적용

[#543]

* refactor: 추상화된 fetch함수 사용

[#543]

* feat: 모든 서버 필터링 정보를 초기화 하는 메서드 구현

[#543]

* refactor: 로그아웃 시 sessionStorage에 저장되던 멤버 정보 초기값 수정

[#543]

* comment: 토큰 액션 메서드 주석 추가

[#543]

* fix: 타입 에러 수정

[#543]

* feat: 자동차 등록 실패시 모달을 닫는 기능 구현

[#543]

* refactor: useMemberFilters 훅 내부 fetch함수 fetchUtils로 변경

[#543]

* refactor: 불필요한 console.log삭제

[#543]

* comment: 완료한 TODO 주석 삭제

[#543]

* refactor: console.log 삭제

[#543]

* refactor: useServerStationFilterActions 객체명 수정

- useServerStationFilterStoreActions

[#543]

* refactor: 컴포넌트 내 복잡한 로직 훅 분리

[#543]

* refactor: fetchMemberFilters 메서드 에러 처리 추가

[#543]

* refactor: getMemberToken 메서드에 fetchUtils 적용

[#543]

* fix: 로그인 로직 진행중 발생한 오류 수정

[#543]

* refactor: 로그인 여부 판별 로직 수정

- 첫 화면 로딩 시 로그인 되어 있다면 "로그인 되었습니다" 토스트 표시
- 첫 화면 로딩 시 로그인 되어 있지 않다면 아무런 토스트도 표시하지 않음

[#543]

* refactor: 로그인 로딩 페이지 수정

[#543]
  • Loading branch information
kyw0716 authored Aug 17, 2023
1 parent b506b35 commit 89ed6dd
Show file tree
Hide file tree
Showing 16 changed files with 291 additions and 223 deletions.
8 changes: 0 additions & 8 deletions frontend/src/components/google-maps/map/CarFfeineMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { setLocalStorage } from '@utils/storage';

import { getGoogleMapStore } from '@stores/google-maps/googleMapStore';
import { toastActions } from '@stores/layout/toastStore';
import { memberTokenStore } from '@stores/login/memberTokenStore';
import { serverStationFilterAction } from '@stores/station-filters/serverStationFiltersStore';

import { useMemberFilters } from '@hooks/tanstack-query/station-filters/useMemberFilters';
Expand Down Expand Up @@ -73,18 +72,11 @@ const CarFfeineMapListener = () => {
return <></>;
};

// TODO: 유저 필터링이 로그인 된 이후 바로 적용되지 않는 문제 다시 발생. 이 부분 어떻게 수정할지 고민해보기
const UserFilterListener = () => {
const queryClient = useQueryClient();
const { data: memberFilters } = useMemberFilters();
const { setAllServerStationFilters } = serverStationFilterAction;

console.log('현재 로그인한 유저가 등록한 필터 정보', memberFilters);
console.log(
'클라이언트 전역 상태에 저장된 필터 정보',
serverStationFilterAction.getAllServerStationFilters()
);

if (memberFilters !== undefined) {
setAllServerStationFilters(memberFilters);
queryClient.invalidateQueries([{ queryKey: [QUERY_KEY_STATIONS] }]);
Expand Down
20 changes: 7 additions & 13 deletions frontend/src/components/login/GoogleLogin.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useEffect, useState } from 'react';

import { fetchUtils } from '@utils/fetch';
import { getMemberToken } from '@utils/login';
import { getAPIEndPoint } from '@utils/login/index';
import { setSessionStorage } from '@utils/storage';

import type { MemberInfo } from '@stores/login/memberInfoStore';
import { memberTokenStore } from '@stores/login/memberTokenStore';

import ButtonNext from '@common/ButtonNext';
import FlexBox from '@common/FlexBox';
import Loading from '@common/Loading';
import Text from '@common/Text';

import LogoIcon from '@ui/Svg/LogoIcon';
Expand All @@ -24,12 +26,9 @@ const GoogleLogin = () => {

getMemberToken(code, 'google')
.then(async (token) => {
const memberInfo = await fetch(`${APIEndPoint}/members/me`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
}).then<MemberInfo>((response) => response.json());
memberTokenStore.setState(token);

const memberInfo = await fetchUtils.get(`${APIEndPoint}/members/me`);

setSessionStorage(SESSION_KEY_MEMBER_TOKEN, token);
setSessionStorage(SESSION_KEY_MEMBER_INFO, JSON.stringify(memberInfo));
Expand Down Expand Up @@ -62,12 +61,7 @@ const GoogleLogin = () => {
);
}

return (
<FlexBox height="100vh" direction="column" alignItems="center" justifyContent="center">
<LogoIcon width={15} />
<Text variant="h5">로딩중...</Text>
</FlexBox>
);
return <Loading />;
};

export default GoogleLogin;
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import { ArrowLeftIcon, ArrowPathIcon } from '@heroicons/react/24/outline';
import { css } from 'styled-components';

import { useQueryClient } from '@tanstack/react-query';

import { getAPIEndPoint } from '@utils/login';

import { toastActions } from '@stores/layout/toastStore';
import { memberInfoStore } from '@stores/login/memberInfoStore';
import { memberTokenStore } from '@stores/login/memberTokenStore';
import { serverStationFilterAction } from '@stores/station-filters/serverStationFiltersStore';

import { useServerStationFilters } from '@hooks/tanstack-query/station-filters/useServerStationFilters';
import { useServerStationFilterActions } from '@hooks/useServerStationFilterActions';
import { useServerStationFilterStoreActions } from '@hooks/useServerStationFilterActions';

import Button from '@common/Button';
import ButtonNext from '@common/ButtonNext';
Expand All @@ -22,18 +16,16 @@ import ServerStationFiltersSkeleton from '@ui/ServerStationFilters/ServerStation
import { useNavigationBar } from '@ui/compound/NavigationBar/hooks/useNavigationBar';

import { CONNECTOR_TYPES, COMPANIES } from '@constants/chargers';
import { QUERY_KEY_STATIONS, QUERY_KEY_MEMBER_SELECTED_FILTERS } from '@constants/queryKeys';

import type { Capacity, StationFilters } from '@type';
import type { Capacity } from '@type';

import FilterSection from './FilterOption';
import { useServerStationFiltersComponentActions } from './hooks/useServerStationFiltersComponentActions';

const ServerStationFilters = () => {
const queryClient = useQueryClient();
const { showToast } = toastActions;
const { data: serverStationFilters, isLoading } = useServerStationFilters();
const { closeBasePanel } = useNavigationBar();

const { handleStationsRefetch, submitMemberFilters } = useServerStationFiltersComponentActions();
const {
toggleCapacityFilter,
toggleConnectorTypeFilter,
Expand All @@ -42,7 +34,7 @@ const ServerStationFilters = () => {
getIsConnectorTypeSelected,
getIsCompanySelected,
resetAllFilters,
} = useServerStationFilterActions();
} = useServerStationFilterStoreActions();

if (isLoading) {
return <ServerStationFiltersSkeleton />;
Expand All @@ -51,60 +43,14 @@ const ServerStationFilters = () => {
const { connectorTypes, capacities, companies } = serverStationFilters;

const handleApplySelectedFilters = async () => {
const APIEndPoint = getAPIEndPoint();
const memberId = memberInfoStore.getState()?.memberId;
const memberToken = memberTokenStore.getState();

const { getAllServerStationFilters, setAllServerStationFilters, resetAllServerStationFilters } =
serverStationFilterAction;
const selectedFilters = getAllServerStationFilters();
const { capacities, companies, connectorTypes } = selectedFilters;

if (memberId === undefined) {
queryClient.invalidateQueries({ queryKey: [QUERY_KEY_STATIONS] });
showToast('필터가 적용되었습니다');
const isLoggedInUser = memberTokenStore.getState() === '';

if (isLoggedInUser !== true) {
handleStationsRefetch();
return;
}

try {
const stationFilters = await fetch(`${APIEndPoint}/members/${memberId}/filters`, {
method: 'POST',
headers: {
Authorization: `Bearer ${memberToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
filters: [
...capacities.map((capacity) => ({
type: 'capacity',
name: capacity,
})),
...companies.map((company) => ({
type: 'company',
name: company,
})),
...connectorTypes.map((connectorType) => ({
type: 'connectorType',
name: connectorType,
})),
],
}),
}).then<StationFilters>((response) => response.json());

setAllServerStationFilters(stationFilters);
queryClient.invalidateQueries({ queryKey: [QUERY_KEY_STATIONS] });

showToast('필터가 적용되었습니다');
} catch {
const stationFilters = queryClient.getQueryData<StationFilters>([
QUERY_KEY_MEMBER_SELECTED_FILTERS,
]);
resetAllServerStationFilters(stationFilters);

queryClient.invalidateQueries({ queryKey: [QUERY_KEY_STATIONS] });

showToast('필터 적용에 실패했습니다', 'error');
}
submitMemberFilters();
};

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useQueryClient } from '@tanstack/react-query';

import { fetchUtils } from '@utils/fetch';
import { getAPIEndPoint } from '@utils/login';

import { toastActions } from '@stores/layout/toastStore';
import { memberInfoStore } from '@stores/login/memberInfoStore';
import { serverStationFilterAction } from '@stores/station-filters/serverStationFiltersStore';

import { QUERY_KEY_MEMBER_SELECTED_FILTERS, QUERY_KEY_STATIONS } from '@constants/queryKeys';

import type { StationFilters } from '@type';

export const useServerStationFiltersComponentActions = () => {
const queryClient = useQueryClient();
const { showToast } = toastActions;

const fallbackToPreviousFilters = () => {
const { resetAllServerStationFilters } = serverStationFilterAction;
const stationFilters = queryClient.getQueryData<StationFilters>([
QUERY_KEY_MEMBER_SELECTED_FILTERS,
]);
resetAllServerStationFilters(stationFilters);

queryClient.invalidateQueries({ queryKey: [QUERY_KEY_STATIONS] });

showToast('필터 적용에 실패했습니다', 'error');
};

const handleStationsRefetch = () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEY_STATIONS] });
showToast('필터가 적용되었습니다');
};

const applyMemberFilters = async () => {
const APIEndPoint = getAPIEndPoint();
const memberId = memberInfoStore.getState()?.memberId;

const { getMemberFilterRequestBody } = serverStationFilterAction;
const memberFilterRequestBody = getMemberFilterRequestBody();

return await fetchUtils.post<StationFilters, typeof memberFilterRequestBody>(
`${APIEndPoint}/members/${memberId}/filters`,
memberFilterRequestBody
);
};

const submitMemberFilters = async () => {
const { setAllServerStationFilters } = serverStationFilterAction;

try {
const stationFilters = await applyMemberFilters();

setAllServerStationFilters(stationFilters);
handleStationsRefetch();
} catch {
fallbackToPreviousFilters();
}
};

return {
fallbackToPreviousFilters,
handleStationsRefetch,
applyMemberFilters,
submitMemberFilters,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const ChargerList = ({ chargers, stationId, reportCount }: ChargerListProps) =>
const availableChargersSize = chargers.filter((charger) => charger.state === 'STANDBY').length;
const loadedChargersSize = page * CHARGER_SIZE;

const handleShowMoreChargers = () => {
setPage((prev) => prev + 1);
};

return (
<>
<Text tag="h3" fontSize={1.8} weight="bold" mt={8} mb={1.5}>
Expand All @@ -51,7 +55,7 @@ const ChargerList = ({ chargers, stationId, reportCount }: ChargerListProps) =>
))}
</FlexBox>
{totalChargersSize - loadedChargersSize > 0 && (
<Button css={MoreButtonContainer}>
<Button css={MoreButtonContainer} onClick={handleShowMoreChargers}>
<FlexBox justifyContent="center">
<Text>더보기</Text>
<ChevronDownIcon width={20} />
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/ui/modal/CarModal/CarModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const CarModal = () => {
modalActions.closeModal();
} catch (error) {
toastActions.showToast(error.message, 'error');
modalActions.closeModal();
}
};

Expand Down
80 changes: 17 additions & 63 deletions frontend/src/components/ui/modal/CarModal/fetch/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { fetchUtils } from '@utils/fetch';

import { serverStore } from '@stores/config/serverStore';
import { memberInfoStore } from '@stores/login/memberInfoStore';
import { memberTokenStore } from '@stores/login/memberTokenStore';
import { serverStationFilterAction } from '@stores/station-filters/serverStationFiltersStore';

import { SERVERS } from '@constants';
Expand All @@ -18,25 +19,12 @@ import type { Car } from '@type/cars';
export const submitMemberCar = async (carName: string, vintage: string): Promise<Car> => {
const mode = serverStore.getState();
const memberId = memberInfoStore.getState()?.memberId;
const memberToken = memberTokenStore.getState();

const memberCarInfo = await fetch(`${SERVERS[mode]}/members/${memberId}/cars`, {
method: 'POST',
headers: {
Authorization: `Bearer ${memberToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: carName, vintage }),
}).then<Car>((response) => {
if (!response.ok) {
if (response.status === 401) {
throw new Error('로그인이 필요합니다');
}
throw new Error('차량 정보를 등록하는 중에 오류가 발생했습니다');
}

return response.json();
});
const memberCarInfo = await fetchUtils.post<Car, Omit<Car, 'carId'>>(
`${SERVERS[mode]}/members/${memberId}/cars`,
{ name: carName, vintage },
'차량 정보를 등록하는 중에 오류가 발생했습니다'
);

return memberCarInfo;
};
Expand All @@ -50,13 +38,9 @@ export const submitMemberCar = async (carName: string, vintage: string): Promise
export const getCarFilters = async (carId: number): Promise<StationFilters> => {
const mode = serverStore.getState();

const carFilters = await fetch(`${SERVERS[mode]}/cars/${carId}/filters`).then<StationFilters>(
(response) => {
if (!response.ok) {
throw new Error('차량 필터 정보를 불러오는 중에 에러가 발생했습니다');
}
return response.json();
}
const carFilters = await fetchUtils.get<StationFilters>(
`${SERVERS[mode]}/cars/${carId}/filters`,
'차량 필터 정보를 불러오는 중에 에러가 발생했습니다'
);

return carFilters;
Expand All @@ -71,45 +55,15 @@ export const getCarFilters = async (carId: number): Promise<StationFilters> => {
export const submitMemberFilters = async (carFilters: StationFilters) => {
const mode = serverStore.getState();
const memberId = memberInfoStore.getState()?.memberId;
const memberToken = memberTokenStore.getState();

const { setAllServerStationFilters, getAllServerStationFilters } = serverStationFilterAction;
const { setAllServerStationFilters, getMemberFilterRequestBody } = serverStationFilterAction;
setAllServerStationFilters(carFilters);
const memberFilterRequestBody = getMemberFilterRequestBody();

const { capacities, companies, connectorTypes } = getAllServerStationFilters();

const memberFilters = await fetch(`${SERVERS[mode]}/members/${memberId}/filters`, {
method: 'POST',
headers: {
Authorization: `Bearer ${memberToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
filters: [
...capacities.map((capacity) => ({
type: 'capacity',
name: capacity,
})),
...companies.map((company) => ({
type: 'company',
name: company,
})),
...connectorTypes.map((connectorType) => ({
type: 'connectorType',
name: connectorType,
})),
],
}),
}).then<StationFilters>((response) => {
if (!response.ok) {
if (response.status === 401) {
throw new Error('로그인이 필요합니다');
}
throw new Error('필터링 정보를 저장하는 중 오류가 발생했습니다');
}

return response.json();
});
const memberFilters = fetchUtils.post<StationFilters, typeof memberFilterRequestBody>(
`${SERVERS[mode]}/members/${memberId}/filters`,
memberFilterRequestBody,
'필터링 정보를 저장하는 중 오류가 발생했습니다'
);

return memberFilters;
};
Loading

0 comments on commit 89ed6dd

Please sign in to comment.