From ac9eac5ed4af52d8b36c38020eed48ea7c7aa537 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Thu, 24 Oct 2024 16:16:10 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20Context=20API=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=B4=EC=84=9C=20=EC=A0=84=EC=97=AD=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=97=90=EB=9F=AC=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EB=A5=BC=20=EA=B4=80=EB=A6=AC=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 에러 상태와 에러 상태를 변경하는 컨텍스를 구분 - QueryClientManager 컴포넌트에서 mutation에서 에러가 발생하면 이를 감지해 에러 상태를 업데이트 - Context API에서 공유받는 데이터를 편하게 사용할 수 있도록, 커스텀 훅으로 useContext를 추상화 --- .../components/QueryClientManager/index.tsx | 27 ++++++++++++++++ frontend/src/contexts/ErrorProvider.tsx | 15 +++++++++ .../useErrorDispatch/useErrorDispatch.ts | 15 +++++++++ .../src/hooks/useErrorState/useErrorState.ts | 15 +++++++++ frontend/src/index.tsx | 31 ++++++++----------- 5 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/QueryClientManager/index.tsx create mode 100644 frontend/src/contexts/ErrorProvider.tsx create mode 100644 frontend/src/hooks/useErrorDispatch/useErrorDispatch.ts create mode 100644 frontend/src/hooks/useErrorState/useErrorState.ts diff --git a/frontend/src/components/QueryClientManager/index.tsx b/frontend/src/components/QueryClientManager/index.tsx new file mode 100644 index 000000000..5485a5457 --- /dev/null +++ b/frontend/src/components/QueryClientManager/index.tsx @@ -0,0 +1,27 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { type PropsWithChildren } from 'react'; + +import useErrorDispatch from '@hooks/useErrorDispatch/useErrorDispatch'; + +import { ResponseError } from '@utils/responseError'; + +export default function QueryClientManager({ children }: PropsWithChildren) { + const setError = useErrorDispatch(); + + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + throwOnError: true, + }, + mutations: { + onError: (error: unknown) => { + if (error instanceof ResponseError) { + setError(error); + } + }, + }, + }, + }); + + return {children}; +} diff --git a/frontend/src/contexts/ErrorProvider.tsx b/frontend/src/contexts/ErrorProvider.tsx new file mode 100644 index 000000000..a7ef1eaab --- /dev/null +++ b/frontend/src/contexts/ErrorProvider.tsx @@ -0,0 +1,15 @@ +import type { PropsWithChildren } from 'react'; +import { createContext, useState } from 'react'; + +export const ErrorStateContext = createContext(null); +export const ErrorDispatchContext = createContext<(error: Error) => void>(() => {}); + +export const ErrorProvider = ({ children }: PropsWithChildren) => { + const [error, setError] = useState(null); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/hooks/useErrorDispatch/useErrorDispatch.ts b/frontend/src/hooks/useErrorDispatch/useErrorDispatch.ts new file mode 100644 index 000000000..3a1c5b7d5 --- /dev/null +++ b/frontend/src/hooks/useErrorDispatch/useErrorDispatch.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react'; + +import { ErrorDispatchContext } from '@contexts/ErrorProvider'; + +const useErrorDispatch = () => { + const setError = useContext(ErrorDispatchContext); + + if (!setError) { + throw new Error('ErrorProvider 내부에서만 해당 훅을 사용할 수 있어요'); + } + + return setError; +}; + +export default useErrorDispatch; diff --git a/frontend/src/hooks/useErrorState/useErrorState.ts b/frontend/src/hooks/useErrorState/useErrorState.ts new file mode 100644 index 000000000..f0c904074 --- /dev/null +++ b/frontend/src/hooks/useErrorState/useErrorState.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react'; + +import { ErrorStateContext } from '@contexts/ErrorProvider'; + +const useErrorState = () => { + const error = useContext(ErrorStateContext); + + if (error === undefined) { + throw new Error('ErrorProvider 내부에서만 해당 훅을 사용할 수 있어요'); + } + + return error; +}; + +export default useErrorState; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 0bfb0a33e..d7cfa3398 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,12 +1,15 @@ import { Global, ThemeProvider } from '@emotion/react'; import * as Sentry from '@sentry/react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import React from 'react'; import ReactDOM from 'react-dom/client'; +import { ErrorProvider } from '@contexts/ErrorProvider'; import ToastProvider from '@contexts/ToastProvider'; +import ErrorToastNotifier from '@components/ErrorToastNotifier'; +import QueryClientManager from '@components/QueryClientManager'; + import globalStyles from '@styles/global'; import theme from '@styles/theme'; @@ -35,28 +38,20 @@ Sentry.init({ enabled: process.env.NODE_ENV !== 'development', }); -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - throwOnError: true, - }, - mutations: { - throwOnError: true, - }, - }, -}); - enableMocking().then(() => { ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - + + + + + + + + + , ); From cec18f9e61aa7ea300878262ad885a93e6449bf3 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Thu, 24 Oct 2024 16:28:25 +0900 Subject: [PATCH 2/8] =?UTF-8?q?refactor:=20fetchClient=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD,=20fetcher?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetchClient 함수에서 데이터를 반환하는 것이 아니라, 서버의 응답 객체를 반환하는 것으로 수정 - 예외가 발생했을 경우에는 ResponseError 객체를 생성해서 에러를 throw - get, post, postWithResponse, delete 메서드 생성 --- frontend/src/apis/_common/fetchClient.ts | 54 +++++++++++++----------- frontend/src/apis/_common/fetcher.ts | 31 ++++++++++++++ 2 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 frontend/src/apis/_common/fetcher.ts diff --git a/frontend/src/apis/_common/fetchClient.ts b/frontend/src/apis/_common/fetchClient.ts index b36444c54..1ee9aa728 100644 --- a/frontend/src/apis/_common/fetchClient.ts +++ b/frontend/src/apis/_common/fetchClient.ts @@ -4,11 +4,11 @@ import { BASE_URL } from '@constants/api'; export type HTTPMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE'; -interface FetchOption { +export interface FetchOption { path: string; method: HTTPMethod; - errorMessage?: string; body?: object; + headers?: HeadersInit; /** * @Yoonkyoungme * isAuthRequire: 인증이 필요한 API 요청인지를 나타내는 플래그 @@ -21,29 +21,35 @@ interface FetchOption { // TODO: TypeError: Failed to Fetch에 대한 에러 처리는 어떻게 할 예정인지. const createFetchClient = (baseUrl: string) => { - return async ({ path, method, body, isAuthRequire }: FetchOption): Promise => { - const url = `${baseUrl}${path}`; - const response = await fetch(url, { - method, - headers: { - 'Content-Type': 'application/json', - }, - body: body ? JSON.stringify(body) : null, - credentials: isAuthRequire ? 'include' : 'omit', - }); - - if (response.status === 401) { - throw new Error('인증이 만료되었습니다. 다시 로그인해주세요.'); + return async ({ path, method, body, isAuthRequire, headers }: FetchOption) => { + try { + const url = `${baseUrl}${path}`; + const response: Response = await fetch(url, { + method, + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + body: body ? JSON.stringify(body) : null, + credentials: isAuthRequire ? 'include' : 'omit', + }); + + // response 객체는 에러가 발생하면 데이터는 응답 객체가 되고, 정상적인 응답이 오면 데이터 객체가 된다. + if (!response.ok) { + // 응답이 에러 객체인 경우 ResponseError 객체를 생성 -> QueryClientManager 컴포넌트에서 에러 상태를 업데이트 + const errorData = await response.json(); + throw new ResponseError(errorData); + } + + return response; + } catch (error) { + // catch network error + if (error instanceof Error) { + throw error; + } + + throw error; } - - // 현재 응답 결과로 받아오는 데이터가 모두 data로 감싸서 전달받는 형태이므로 아래와 같이 구현(@낙타) - const data = await response.json(); - - if (!response.ok) { - throw new ResponseError(data); - } - - return data.data as T; }; }; diff --git a/frontend/src/apis/_common/fetcher.ts b/frontend/src/apis/_common/fetcher.ts new file mode 100644 index 000000000..ea9317b2a --- /dev/null +++ b/frontend/src/apis/_common/fetcher.ts @@ -0,0 +1,31 @@ +import type { FetchOption } from './fetchClient'; +import { fetchClient } from './fetchClient'; + +type FetcherArgs = Omit; + +export const fetcher = { + get: async ({ path, isAuthRequire }: FetcherArgs): Promise => { + const response = await fetchClient({ + path, + method: 'GET', + isAuthRequire, + }); + + const data = await response.json(); + + return data.data as T; + }, + post: async ({ path, body, isAuthRequire = false }: FetcherArgs) => { + await fetchClient({ path, method: 'POST', body, isAuthRequire }); + }, + postWithResponse: async ({ path, body, isAuthRequire = false }: FetcherArgs): Promise => { + const response = await fetchClient({ path, method: 'POST', body, isAuthRequire }); + + const data = await response.json(); + + return data.data as T; + }, + delete: async ({ path, isAuthRequire = false }: FetcherArgs) => { + await fetchClient({ path, method: 'DELETE', isAuthRequire }); + }, +}; From 27b888b5a15da6d2681402b97f5b5bb7066da764 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Thu, 24 Oct 2024 16:29:10 +0900 Subject: [PATCH 3/8] =?UTF-8?q?refactor:=20fetcher=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20=EA=B2=83?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/meetings/confirms.ts | 24 +++------------- frontend/src/apis/meetings/meetings.ts | 15 +++------- frontend/src/apis/meetings/recommends.ts | 10 ++----- frontend/src/apis/schedules.ts | 36 ++++++------------------ frontend/src/apis/users.ts | 26 ++--------------- 5 files changed, 23 insertions(+), 88 deletions(-) diff --git a/frontend/src/apis/meetings/confirms.ts b/frontend/src/apis/meetings/confirms.ts index 6015e8589..5e2ae0f1d 100644 --- a/frontend/src/apis/meetings/confirms.ts +++ b/frontend/src/apis/meetings/confirms.ts @@ -1,6 +1,4 @@ -import { BASE_URL } from '@constants/api'; - -import { fetchClient } from '../_common/fetchClient'; +import { fetcher } from '../_common/fetcher'; import type { MeetingType } from './meetings'; export interface ConfirmDates { @@ -25,9 +23,8 @@ export interface GetConfirmedMeetingInfoResponse extends ConfirmDates { } export const postMeetingConfirm = async ({ uuid, requests }: PostMeetingConfirmRequest) => { - const data = await fetchClient({ + const data = await fetcher.post({ path: `/${uuid}/confirm`, - method: 'POST', body: requests, isAuthRequire: true, }); @@ -36,24 +33,11 @@ export const postMeetingConfirm = async ({ uuid, requests }: PostMeetingConfirmR }; export const getConfirmedMeetingInfo = async (uuid: string) => { - const data = await fetchClient>({ - path: `/${uuid}/confirm`, - method: 'GET', - }); + const data = await fetcher.get({ path: `/${uuid}/confirm` }); return data; }; export const deleteFixedMeeting = async (uuid: string) => { - const response = await fetch(`${BASE_URL}/${uuid}/confirm`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - }); - - if (!response.ok) { - throw new Error('약속을 확정 취소하는데 실패했어요. :('); - } + await fetcher.delete({ path: `/${uuid}/confirm`, isAuthRequire: true }); }; diff --git a/frontend/src/apis/meetings/meetings.ts b/frontend/src/apis/meetings/meetings.ts index 6cb6088a6..a4d1f3a70 100644 --- a/frontend/src/apis/meetings/meetings.ts +++ b/frontend/src/apis/meetings/meetings.ts @@ -4,7 +4,7 @@ import { ResponseError } from '@utils/responseError'; import { BASE_URL } from '@constants/api'; -import { fetchClient } from '../_common/fetchClient'; +import { fetcher } from '../_common/fetcher'; export type MeetingType = 'DAYSONLY' | 'DATETIME'; @@ -51,10 +51,7 @@ interface PostMeetingResponse { export const getMeetingBase = async (uuid: string): Promise => { const path = `/${uuid}`; - const data = await fetchClient({ - path, - method: 'GET', - }); + const data = await fetcher.get({ path }); return { meetingName: data.meetingName, @@ -89,9 +86,8 @@ interface PostMeetingResponse { } export const postMeeting = async (request: PostMeetingRequest): Promise => { - const data = await fetchClient({ + const data = await fetcher.postWithResponse({ path: '', - method: 'POST', body: request, isAuthRequire: true, }); @@ -144,10 +140,7 @@ interface MeetingEntranceDetails { } export const getMeetingEntranceDetails = async (uuid: string) => { - const data = await fetchClient({ - path: `/${uuid}/home`, - method: 'GET', - }); + const data = await fetcher.get({ path: `/${uuid}/home` }); return data; }; diff --git a/frontend/src/apis/meetings/recommends.ts b/frontend/src/apis/meetings/recommends.ts index a80230c5a..ddde80544 100644 --- a/frontend/src/apis/meetings/recommends.ts +++ b/frontend/src/apis/meetings/recommends.ts @@ -1,4 +1,4 @@ -import { fetchClient } from '../_common/fetchClient'; +import { fetcher } from '../_common/fetcher'; import type { MeetingType } from './meetings'; interface GetMeetingRecommendRequest { @@ -35,10 +35,7 @@ export const getMeetingTimeRecommends = async ({ const path = `/${uuid}/recommended-schedules?${urlParams.toString()}`; - const data = await fetchClient({ - path, - method: 'GET', - }); + const data = await fetcher.get({ path }); return data; }; @@ -54,9 +51,8 @@ export const getMeetingAttendees = async ({ }): Promise => { const path = `/${uuid}/attendees`; - const data = await fetchClient({ + const data = await fetcher.get({ path, - method: 'GET', }); return data; diff --git a/frontend/src/apis/schedules.ts b/frontend/src/apis/schedules.ts index 59cfc1889..e304a34b5 100644 --- a/frontend/src/apis/schedules.ts +++ b/frontend/src/apis/schedules.ts @@ -5,11 +5,7 @@ import type { MeetingSingleSchedule, } from 'types/schedule'; -import { ResponseError } from '@utils/responseError'; - -import { BASE_URL } from '@constants/api'; - -import { fetchClient } from './_common/fetchClient'; +import { fetcher } from './_common/fetcher'; export interface PostScheduleRequest { uuid: string; @@ -17,22 +13,13 @@ export interface PostScheduleRequest { } export const postSchedule = async ({ uuid, requestData }: PostScheduleRequest) => { - const response = await fetch(`${BASE_URL}/${uuid}/schedules`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + await fetcher.post({ + path: `/${uuid}/schedules`, + body: { dateTimes: requestData, - }), - credentials: 'include', + }, + isAuthRequire: true, }); - - if (!response.ok) { - const data = await response.json(); - - throw new ResponseError(data); - } }; export const createMeetingSchedulesRequestUrl = (uuid: string, attendeeName: string) => { @@ -50,10 +37,7 @@ interface MeetingAllSchedulesResponse { const getMeetingAllSchedules = async (uuid: string): Promise => { const path = `/${uuid}/schedules`; - const data = await fetchClient({ - path, - method: 'GET', - }); + const data = await fetcher.get({ path }); return { schedules: data.schedules, @@ -74,9 +58,8 @@ const getMeetingSingleSchedule = async ({ }): Promise => { const path = createMeetingSchedulesRequestUrl(uuid, attendeeName); - const data = await fetchClient({ + const data = await fetcher.get({ path, - method: 'GET', }); return { @@ -88,9 +71,8 @@ const getMeetingSingleSchedule = async ({ export const getMeetingMySchedule = async (uuid: string): Promise => { const path = `/${uuid}/attendees/me/schedules`; - const data = await fetchClient({ + const data = await fetcher.get({ path, - method: 'GET', isAuthRequire: true, }); diff --git a/frontend/src/apis/users.ts b/frontend/src/apis/users.ts index 12cc212d2..81b87bacf 100644 --- a/frontend/src/apis/users.ts +++ b/frontend/src/apis/users.ts @@ -1,8 +1,6 @@ -import { ResponseError } from '@utils/responseError'; - import { BASE_URL } from '@constants/api'; -import { fetchClient } from './_common/fetchClient'; +import { fetcher } from './_common/fetcher'; interface UserLoginRequest { uuid: string; @@ -13,9 +11,8 @@ interface UserLoginRequest { } export const postUserLogin = async ({ uuid, request }: UserLoginRequest) => { - const data = await fetchClient({ + const data = await fetcher.postWithResponse({ path: `/${uuid}/login`, - method: 'POST', body: request, isAuthRequire: true, }); @@ -30,22 +27,5 @@ export const postUserLogin = async ({ uuid, request }: UserLoginRequest) => { * TODO: 응답 데이터가 없을 때도 대응 가능한 fetchClient 함수를 만들어야 함 */ export const postUserLogout = async (uuid: string) => { - try { - const response = await fetch(`${BASE_URL}/${uuid}/logout`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - }); - - if (!response.ok) { - const data = await response.json(); - - throw new ResponseError(data); - } - } catch (error) { - console.error('로그아웃 중 문제가 발생했습니다:', error); - throw error; - } + await fetcher.post({ path: `${BASE_URL}/${uuid}/logout`, isAuthRequire: true }); }; From 58a4547d56000a567c698d9655bfa2db2696209b Mon Sep 17 00:00:00 2001 From: hwinkr Date: Thu, 24 Oct 2024 16:32:58 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A5=BC=20=EA=B5=AC=EB=8F=85=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=ED=86=A0=EC=8A=A4=ED=8A=B8=20UI=EB=A1=9C=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=EC=9D=84=20=EC=A0=84=EB=8B=AC=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ErrorToastNotifier/index.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 frontend/src/components/ErrorToastNotifier/index.tsx diff --git a/frontend/src/components/ErrorToastNotifier/index.tsx b/frontend/src/components/ErrorToastNotifier/index.tsx new file mode 100644 index 000000000..604a623ac --- /dev/null +++ b/frontend/src/components/ErrorToastNotifier/index.tsx @@ -0,0 +1,23 @@ +import type { PropsWithChildren } from 'react'; +import { useEffect, useRef } from 'react'; + +import useErrorState from '@hooks/useErrorState/useErrorState'; +import useToast from '@hooks/useToast/useToast'; + +export default function ErrorToastNotifier({ children }: PropsWithChildren) { + const error = useErrorState(); + const { addToast } = useToast(); + + const addToastCallbackRef = useRef< + (({ type, message, duration }: Parameters[0]) => void) | null + >(null); + addToastCallbackRef.current = addToast; + + useEffect(() => { + if (!error || !addToastCallbackRef.current) return; + + addToastCallbackRef.current({ type: 'warning', message: error.message, duration: 3000 }); + }, [error]); + + return children; +} From 4f824b5e9e5b22a537f887aac9c6de5965035890 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Thu, 24 Oct 2024 16:34:40 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=EC=95=BD=EC=86=8D=EC=9D=84=20?= =?UTF-8?q?=EC=9E=A0=EA=B7=B8=EA=B3=A0=20=EC=95=BD=EC=86=8D=20=ED=99=95?= =?UTF-8?q?=EC=A0=95=ED=95=98=EB=9F=AC=20=EA=B0=88=20=EA=B2=83=EC=9D=B8?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=EB=AC=BB=EB=8A=94=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ScheduleViewer/SchedulesViewer.tsx | 53 ++++++++++++++++--- .../Modal/MeetingLockConfirmModal/index.tsx | 41 ++++++++++++++ 2 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/_common/Modal/MeetingLockConfirmModal/index.tsx diff --git a/frontend/src/components/Schedules/ScheduleViewer/SchedulesViewer.tsx b/frontend/src/components/Schedules/ScheduleViewer/SchedulesViewer.tsx index 2bc93a980..06a470a49 100644 --- a/frontend/src/components/Schedules/ScheduleViewer/SchedulesViewer.tsx +++ b/frontend/src/components/Schedules/ScheduleViewer/SchedulesViewer.tsx @@ -7,10 +7,15 @@ import { UuidContext } from '@contexts/UuidProvider'; import { Button } from '@components/_common/Buttons/Button'; import TabButton from '@components/_common/Buttons/TabButton'; +import MeetingLockConfirmModal from '@components/_common/Modal/MeetingLockConfirmModal'; +import Text from '@components/_common/Text'; +import useConfirmModal from '@hooks/useConfirmModal/useConfirmModal'; import useRouter from '@hooks/useRouter/useRouter'; import useSelectSchedule from '@hooks/useSelectSchedule/useSelectSchedule'; +import { useLockMeetingMutation } from '@stores/servers/meeting/mutations'; + import Check from '@assets/images/attendeeCheck.svg'; import Pen from '@assets/images/pen.svg'; @@ -45,6 +50,9 @@ export default function SchedulesViewer({ const { handleToggleIsTimePickerUpdate } = useContext(TimePickerUpdateStateContext); const { isLoggedIn, userName } = useContext(AuthContext).state; + const { mutate: lockMutate } = useLockMeetingMutation(); + const { isConfirmModalOpen, onToggleConfirmModal } = useConfirmModal(); + const { currentDates, increaseDatePage, @@ -66,6 +74,22 @@ export default function SchedulesViewer({ handleToggleIsTimePickerUpdate(); }; + const routerToMeetingConfirmPage = () => routeTo(`/meeting/${uuid}/confirm`); + + const handleConfirmPageRoute = () => { + if (!isLocked) { + onToggleConfirmModal(); + return; + } + + routerToMeetingConfirmPage(); + }; + + const handleMeetingLockWithRoute = () => { + lockMutate(uuid); + routerToMeetingConfirmPage(); + }; + return ( <>
@@ -110,12 +134,8 @@ export default function SchedulesViewer({
{hostName === userName ? ( - ) : ( )}
@@ -131,6 +151,25 @@ export default function SchedulesViewer({
+ + + + 약속을 확정하기 위해서는 + + + + 약속을 잠그고 약속 확정 페이지로 이동할까요? + + ); } diff --git a/frontend/src/components/_common/Modal/MeetingLockConfirmModal/index.tsx b/frontend/src/components/_common/Modal/MeetingLockConfirmModal/index.tsx new file mode 100644 index 000000000..1aadb13a6 --- /dev/null +++ b/frontend/src/components/_common/Modal/MeetingLockConfirmModal/index.tsx @@ -0,0 +1,41 @@ +import type { PropsWithChildren } from 'react'; +import React from 'react'; + +import { Modal } from '..'; +import { s_button, s_primary, s_secondary } from '../ConfirmModal/ConfirmModal.styles'; +import type { ButtonPositionType } from '../Footer'; +import type { ModalProps } from '../ModalContainer'; + +interface ConfirmModalProps extends ModalProps { + title: string; + buttonPosition?: ButtonPositionType; + onConfirm: () => void; + onSecondButtonClick: () => void; +} + +export default function MeetingLockConfirmModal({ + isOpen, + onClose, + onConfirm, + onSecondButtonClick, + title, + position, + size, + children, + buttonPosition = 'row', +}: PropsWithChildren) { + return ( + + {title} + {children} + + + + + + ); +} From 8e5d8acaa872d53f888acaceb7bcb1fa387df9bb Mon Sep 17 00:00:00 2001 From: hwinkr Date: Thu, 24 Oct 2024 16:37:33 +0900 Subject: [PATCH 6/8] =?UTF-8?q?refactor:=20AuthProvider=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=EC=A0=81=EC=9C=BC=EB=A1=9C=20useParams=20=ED=9B=85?= =?UTF-8?q?=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20=EA=B2=83?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 UuidContext를 사용하면 undefined로 참조되는 문제를 해결하기 위해 예외 처리 --- frontend/src/contexts/AuthProvider.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/contexts/AuthProvider.tsx b/frontend/src/contexts/AuthProvider.tsx index d4528dec9..0b7c45ebc 100644 --- a/frontend/src/contexts/AuthProvider.tsx +++ b/frontend/src/contexts/AuthProvider.tsx @@ -1,10 +1,9 @@ import type { ReactNode } from 'react'; -import { createContext, useContext, useEffect, useState } from 'react'; +import { createContext, useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; import { loadAuthState, saveAuthState } from '@utils/auth'; -import { UuidContext } from './UuidProvider'; - interface AuthState { isLoggedIn: boolean; userName: string; @@ -27,7 +26,8 @@ interface AuthProviderProps { } export const AuthProvider = ({ children }: AuthProviderProps) => { - const { uuid } = useContext(UuidContext); + const params = useParams<{ uuid?: string }>(); + const uuid = params.uuid; const [isLoggedIn, setIsLoggedIn] = useState(() => { if (uuid) { @@ -49,7 +49,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { if (uuid) { saveAuthState(uuid, { isLoggedIn, userName }); } - }, [isLoggedIn, userName]); + }, [isLoggedIn, userName, uuid]); const value: AuthContextType = { state: { isLoggedIn, userName }, From e0275f9a2abc5a5fa65a7c9f24e4e07653ca8d2f Mon Sep 17 00:00:00 2001 From: hwinkr Date: Thu, 24 Oct 2024 16:37:42 +0900 Subject: [PATCH 7/8] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EC=83=81=ED=83=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/stores/servers/meeting/mutations.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/frontend/src/stores/servers/meeting/mutations.ts b/frontend/src/stores/servers/meeting/mutations.ts index eaaf9d435..aed5912d0 100644 --- a/frontend/src/stores/servers/meeting/mutations.ts +++ b/frontend/src/stores/servers/meeting/mutations.ts @@ -1,6 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useContext, useState } from 'react'; -import type { PostMeetingResult } from 'types/meeting'; +import { useContext } from 'react'; import { AuthContext } from '@contexts/AuthProvider'; @@ -16,21 +15,11 @@ export const usePostMeetingMutation = () => { const authContext = useContext(AuthContext); const { setIsLoggedIn, setUserName } = authContext.actions; - const [meetingInfo, setMeetingInfo] = useState({ - uuid: '', - userName: '', - meetingName: '', - firstTime: '', - lastTime: '', - availableDates: [], - }); - const mutation = useMutation({ mutationFn: postMeeting, onSuccess: (responseData) => { const { uuid, userName } = responseData; - setMeetingInfo(responseData); setIsLoggedIn(true); setUserName(userName); @@ -38,7 +27,7 @@ export const usePostMeetingMutation = () => { }, }); - return { mutation, meetingInfo }; + return { mutation }; }; export const useLockMeetingMutation = () => { From f381a55fe378f8c3946c654e30b86370011662cf Mon Sep 17 00:00:00 2001 From: hwinkr Date: Thu, 24 Oct 2024 17:32:01 +0900 Subject: [PATCH 8/8] =?UTF-8?q?chore:=20=EA=B0=95=EC=A1=B0=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Schedules/ScheduleViewer/SchedulesViewer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Schedules/ScheduleViewer/SchedulesViewer.tsx b/frontend/src/components/Schedules/ScheduleViewer/SchedulesViewer.tsx index 06a470a49..d39428c89 100644 --- a/frontend/src/components/Schedules/ScheduleViewer/SchedulesViewer.tsx +++ b/frontend/src/components/Schedules/ScheduleViewer/SchedulesViewer.tsx @@ -163,8 +163,8 @@ export default function SchedulesViewer({ buttonPosition="column" > - 약속을 확정하기 위해서는 - + 약속을 확정하기 위해서는 우선 + 약속을 잠그고 약속 확정 페이지로 이동할까요?