diff --git a/client/.babelrc b/client/.babelrc
index 057c3c385..e970ef544 100644
--- a/client/.babelrc
+++ b/client/.babelrc
@@ -16,6 +16,12 @@
"displayName": true
}
],
- "@babel/plugin-transform-runtime"
+ "@babel/plugin-transform-runtime",
+ [
+ "babel-plugin-transform-builtin-extend",
+ {
+ "globals": ["Error", "Array"]
+ }
+ ]
]
}
diff --git a/client/package.json b/client/package.json
index 5e94fc1b1..c522742f2 100644
--- a/client/package.json
+++ b/client/package.json
@@ -33,8 +33,11 @@
"@tanstack/react-query-devtools": "^4.29.19",
"ajv": "^8.12.0",
"axios": "^1.4.0",
+ "babel-plugin-transform-builtin-extend": "^1.1.2",
+ "ck-util-components": "^1.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-lightweight-form": "^1.2.5",
"react-router-dom": "^6.14.1",
"styled-components": "^6.0.0"
},
diff --git a/client/src/App.tsx b/client/src/App.tsx
index faca6a168..8639ae8d3 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -8,19 +8,19 @@ import SignUpPage from '@pages/signUpPage/SignUpPage';
import LoginPage from '@pages/loginPage/LoginPage';
import PageLayout from '@components/_common/pageLayout/PageLayout';
import RoadmapListPage from '@pages/roadmapListPage/roadmapListPage';
-import Fallback from '@components/_common/fallback/Fallback';
import RoadmapDetailPage from './pages/roadmapDetailPage/RoadmapDetailPage';
import RoadmapCreatePage from './pages/roadmapCreatePage/RoadmapCreatePage';
-import ToastProvider from '@components/_common/toastProvider/ToastProvider';
+import ToastProvider from '@/components/_common/toast/Toast';
import MyPage from '@pages/myPage/MyPage';
import UserInfoProvider from './components/_providers/UserInfoProvider';
import RoadmapSearchResult from './components/roadmapListPage/roadmapSearch/RoadmapSearchResult';
import MainPage from '@pages/mainPage/MainPage';
import OAuthRedirect from './components/loginPage/OAuthRedirect';
-import AsyncBoundary from './components/_common/errorBoundary/AsyncBoundary';
import SessionHandler from '@components/_common/sessionHandler/SessionHandler';
import RouteChangeTracker from '@components/_common/routeChangeTracker/RouteChangeTracker';
import PrivateRouter from '@components/_common/privateRouter/PrivateRouter';
+import { Spinner } from './components/_common/spinner/Spinner.styles';
+import { CriticalErrorBoundary } from './components/_common/errorBoundary/CriticalErrorBoundary';
const GoalRoomDashboardPage = lazy(
() => import('@pages/goalRoomDashboardPage/GoalRoomDashboardPage')
@@ -40,8 +40,8 @@ const App = () => {
-
-
+
+
} />
} />
@@ -52,18 +52,15 @@ const App = () => {
element={}
/>
+ } />
}>
-
+ }>
+
}
/>
- }
- />
{
path='/roadmap/:id/goalroom-create'
element={
-
+ }>
+
+
}
/>
}
+ element={
+ }>
+
+
+ }
/>
{
/>
} />
-
-
+
+
diff --git a/client/src/apis/goalRoom.ts b/client/src/apis/goalRoom.ts
index 392e5c6e5..59c212ef6 100644
--- a/client/src/apis/goalRoom.ts
+++ b/client/src/apis/goalRoom.ts
@@ -15,6 +15,7 @@ import {
} from '@/myTypes/goalRoom/remote';
import client from '@apis/axios/client';
import { GoalRoomRecruitmentStatus, MyPageGoalRoom } from '@myTypes/goalRoom/internal';
+import { API_PATH } from '@/constants/_common/api';
export const getGoalRoomList = async ({
roadmapId,
@@ -24,7 +25,7 @@ export const getGoalRoomList = async ({
lastId,
}: GoalRoomListRequest): Promise => {
const { data } = await client.get(
- `/roadmaps/${roadmapId}/goal-rooms`,
+ API_PATH.GOALROOMS(roadmapId),
{
params: {
...(lastId && { lastId }),
@@ -39,9 +40,7 @@ export const getGoalRoomList = async ({
};
export const getMyGoalRoomList = async (statusCond: GoalRoomRecruitmentStatus) => {
- const { data } = await client.get(
- `/goal-rooms/me?statusCond=${statusCond}`
- );
+ const { data } = await client.get(API_PATH.MY_GOALROOMS(statusCond));
return data;
};
@@ -49,26 +48,28 @@ export const getMyGoalRoomList = async (statusCond: GoalRoomRecruitmentStatus) =
export const getGoalRoomDetail = async (
goalRoomId: number
): Promise => {
- const { data } = await client.get(`/goal-rooms/${goalRoomId}`);
+ const { data } = await client.get(
+ API_PATH.GOALROOM_DETAIL(goalRoomId)
+ );
return data;
};
export const getGoalRoomDashboard = async (goalRoomId: string) => {
const { data } = await client.get(
- `/goal-rooms/${goalRoomId}/me`
+ API_PATH.GOALROOM_DASHBOARD(goalRoomId)
);
return data;
};
export const postCreateGoalRoom = async (body: CreateGoalRoomRequest) => {
- const { data } = await client.post(`/goal-rooms`, body);
+ const { data } = await client.post(API_PATH.CREATE_GOALROOM, body);
return data;
};
export const getGoalRoomTodos = async (goalRoomId: string) => {
const { data } = await client.get(
- `/goal-rooms/${goalRoomId}/todos`
+ `${API_PATH.GOALROOM_TODOS(goalRoomId)}/90`
);
return data;
@@ -78,18 +79,18 @@ export const postToChangeTodoCheckStatus = async ({
goalRoomId,
todoId,
}: GoalRoomTodoChangeStatusRequest) => {
- return client.post(`/goal-rooms/${goalRoomId}/todos/${todoId}`);
+ return client.post(API_PATH.CHANGE_TODO_CHECKS(goalRoomId, todoId));
};
export const postCreateNewTodo = (goalRoomId: string, body: newTodoPayload) => {
- return client.post(`/goal-rooms/${goalRoomId}/todos`, body);
+ return client.post(API_PATH.CREATE_TODO(goalRoomId), body);
};
export const postCreateNewCertificationFeed = (
goalRoomId: string,
formData: FormData
) => {
- return client.post(`/goal-rooms/${goalRoomId}/checkFeeds`, formData, {
+ return client.post(API_PATH.CREATE_FEED(goalRoomId), formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
@@ -97,7 +98,7 @@ export const postCreateNewCertificationFeed = (
};
export const postJoinGoalRoom = (goalRoomId: string) => {
- return client.post(`/goal-rooms/${goalRoomId}/join`);
+ return client.post(API_PATH.JOIN_GOALROOM(goalRoomId));
};
export const getGoalRoomParticipants = async (
@@ -105,7 +106,7 @@ export const getGoalRoomParticipants = async (
participantsSortOrder: ParticipantsSortOrder
) => {
const { data } = await client.get(
- `/goal-rooms/${goalRoomId}/members`,
+ API_PATH.GOALROOM_PARTICIPANTS(goalRoomId),
{
params: {
sortCond: participantsSortOrder,
@@ -118,19 +119,19 @@ export const getGoalRoomParticipants = async (
export const getCertificationFeeds = async (goalRoomId: string) => {
const { data } = await client.get(
- `/goal-rooms/${goalRoomId}/checkFeeds`
+ API_PATH.GOALROOM_FEEDS(goalRoomId)
);
return data;
};
export const postStartGoalRoom = async (goalRoomId: string) => {
- return client.post(`/goal-rooms/${goalRoomId}/start`);
+ return client.post(API_PATH.START_GOALROOM(goalRoomId));
};
export const getGoalRoomNodeList = async (goalRoomId: string) => {
const { data } = await client.get(
- `/goal-rooms/${goalRoomId}/nodes`
+ API_PATH.GOALROOM_NODE_LIST(goalRoomId)
);
return data;
};
diff --git a/client/src/apis/roadmap.ts b/client/src/apis/roadmap.ts
index 5dd53744e..6ddfbd9eb 100644
--- a/client/src/apis/roadmap.ts
+++ b/client/src/apis/roadmap.ts
@@ -1,3 +1,4 @@
+import { API_PATH } from '@/constants/_common/api';
import type {
RoadmapDetailResponse,
RoadmapListRequest,
@@ -12,7 +13,7 @@ export const getRoadmapList = async ({
filterCond,
lastId,
}: RoadmapListRequest) => {
- const { data } = await client.get(`/roadmaps`, {
+ const { data } = await client.get(API_PATH.ROADMAPS, {
params: {
...(categoryId && { categoryId }),
...(lastId && { lastId }),
@@ -31,7 +32,7 @@ export const getSearchRoadmapList = async ({
lastId = '',
size = 10,
}: any) => {
- const { data } = await client.get(`/roadmaps/search`, {
+ const { data } = await client.get(API_PATH.ROADMAP_SEARCH, {
params: {
[category]: search,
filterCond,
@@ -44,22 +45,26 @@ export const getSearchRoadmapList = async ({
};
export const getRoadmapDetail = async (id: number): Promise => {
- const { data } = await client.get(`/roadmaps/${id}`);
+ const { data } = await client.get(API_PATH.ROADMAP_DETAIL(id));
return data;
};
export const postCreateRoadmap = (roadmapValue: FormData) => {
- const resposne = client.post('/roadmaps', roadmapValue, {
- headers: {
- 'Content-Type': 'multipart/form-data;charset=UTF-8',
- },
- });
+ const resposne = client.post(
+ API_PATH.CREATE_ROADMAP,
+ roadmapValue,
+ {
+ headers: {
+ 'Content-Type': 'multipart/form-data;charset=UTF-8',
+ },
+ }
+ );
return resposne;
};
export const getMyRoadmapList = async (size: number, lastId?: number) => {
- const { data } = await client.get('/roadmaps/me', {
+ const { data } = await client.get(API_PATH.MY_ROADMAPS, {
params: {
size,
...(lastId && { lastId }),
diff --git a/client/src/apis/user.ts b/client/src/apis/user.ts
index 65d7ec549..38d634783 100644
--- a/client/src/apis/user.ts
+++ b/client/src/apis/user.ts
@@ -1,3 +1,4 @@
+import { API_PATH } from '@/constants/_common/api';
import client from '@apis/axios/client';
import {
MemberJoinRequest,
@@ -9,17 +10,19 @@ import {
} from '@myTypes/user/remote';
export const signUp = (body: MemberJoinRequest) => {
- return client.post('/members/join', body);
+ return client.post(API_PATH.SIGN_UP, body);
};
export const getNaverLoginRedirectUrl = async () => {
- const { data } = await client.get('/auth/oauth/naver');
+ const { data } = await client.get(
+ API_PATH.NAVER_LOGIN_REDIRECT
+ );
return data;
};
export const naverOAuthToken = async (code: string, state: string) => {
- const { data } = await client.get('/auth/login/oauth', {
+ const { data } = await client.get(API_PATH.NAVER_TOKEN, {
params: {
code,
state,
@@ -30,11 +33,11 @@ export const naverOAuthToken = async (code: string, state: string) => {
};
export const login = (body: UserLoginRequest) => {
- return client.post('/auth/login', body);
+ return client.post(API_PATH.LOGIN, body);
};
export const getUserInfo = (signal?: AbortSignal) => {
- return client.get('/members/me', {
+ return client.get(API_PATH.USER_INFO, {
signal,
});
};
diff --git a/client/src/assets/icons/.gitkeep b/client/src/assets/icons/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/client/src/assets/icons/errorToastButton.svg b/client/src/assets/icons/errorToastButton.svg
new file mode 100644
index 000000000..86c455532
--- /dev/null
+++ b/client/src/assets/icons/errorToastButton.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/assets/icons/successToastButton.svg b/client/src/assets/icons/successToastButton.svg
new file mode 100644
index 000000000..802ed5650
--- /dev/null
+++ b/client/src/assets/icons/successToastButton.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/assets/icons/warningToastButton.svg b/client/src/assets/icons/warningToastButton.svg
new file mode 100644
index 000000000..238299e60
--- /dev/null
+++ b/client/src/assets/icons/warningToastButton.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/_common/dialog/dialog.tsx b/client/src/components/_common/dialog/dialog.tsx
deleted file mode 100644
index 6c41a0c54..000000000
--- a/client/src/components/_common/dialog/dialog.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { DialogContext } from '@/context/dialogContext';
-import { getCustomElement } from '@/hooks/_common/compound';
-import { useContextScope } from '@/hooks/_common/useContextScope';
-import { useSwitch } from '@/hooks/_common/useSwitch';
-import { DialogBackdropProps, DialogTriggerProps } from '@/myTypes/_common/dialog';
-import { PropsWithChildren, ReactElement, useEffect } from 'react';
-
-export const DialogBox = ({
- children,
- defaultOpen = false,
-}: PropsWithChildren<{ defaultOpen?: boolean }>) => {
- const {
- isSwitchOn: isOpen,
- turnSwitchOn: openDialog,
- turnSwitchOff: closeDialog,
- } = useSwitch(defaultOpen);
-
- return (
-
- {children}
-
- );
-};
-
-export const DialogTrigger = (props: PropsWithChildren) => {
- const { asChild, children, ...restProps } = props;
- const { isOpen, openDialog, closeDialog } = useContextScope(DialogContext);
-
- const toggleDialog = () => {
- if (isOpen) closeDialog();
- if (!isOpen) openDialog();
- };
-
- if (asChild) {
- return getCustomElement(children as ReactElement, {
- ...restProps,
- onClick: toggleDialog,
- });
- }
-
- return ;
-};
-
-export const DialogBackdrop = (props: PropsWithChildren) => {
- const { asChild = false, children, ...restProps } = props;
- const { isOpen, closeDialog } = useContextScope(DialogContext);
-
- useEffect(() => {
- document.body.style.overflow = isOpen ? 'hidden' : 'auto';
- }, [isOpen]);
-
- if (asChild) {
- return isOpen
- ? getCustomElement(children as ReactElement, { ...restProps, onClick: closeDialog })
- : null;
- }
-
- return isOpen ? : null;
-};
-
-export const DialogContent = ({ children }: PropsWithChildren) => {
- const { isOpen } = useContextScope(DialogContext);
- return isOpen ? <>{children}> : null;
-};
diff --git a/client/src/components/_common/errorBoundary/ApiErrorBoundary.tsx b/client/src/components/_common/errorBoundary/ApiErrorBoundary.tsx
new file mode 100644
index 000000000..df16999b0
--- /dev/null
+++ b/client/src/components/_common/errorBoundary/ApiErrorBoundary.tsx
@@ -0,0 +1,50 @@
+import { AxiosError } from 'axios';
+import { ErrorInfo } from 'react';
+import { Navigate } from 'react-router-dom';
+import { Forbidden, NotFound } from '../fallback/Fallback';
+import ErrorBoundary from './ErrorBoundary';
+import { APIError } from './errors';
+
+export class ApiErrorBoundary extends ErrorBoundary {
+ static getDerivedStateFromError(error: Error): { didCatch: boolean; error: Error } {
+ if (error instanceof APIError) {
+ return { didCatch: true, error };
+ }
+ throw error;
+ }
+
+ componentDidCatch(
+ error: T extends APIError ? AxiosError : Error,
+ errorInfo: ErrorInfo
+ ): void {
+ if (this.props.isCritical) throw error;
+
+ this.props.onError?.(error, errorInfo);
+
+ switch (error.response?.status) {
+ case 401:
+ this.setState({ fallback: });
+ break;
+ case 403:
+ this.setState({ fallback: });
+ break;
+ case 404:
+ this.setState({ fallback: });
+ break;
+ default:
+ break;
+ }
+ }
+
+ render() {
+ const { didCatch, fallback: innerFallback } = this.state;
+ const { children, fallback: custumFallback } = this.props;
+ if (didCatch) {
+ return (
+ custumFallback ??
+ innerFallback ?? 데이터를 요청할 수 없습니다. 잠시후 다시 시도해주세요
+ );
+ }
+ return children;
+ }
+}
diff --git a/client/src/components/_common/errorBoundary/AsyncBoundary.tsx b/client/src/components/_common/errorBoundary/AsyncBoundary.tsx
index cb4b6c68f..4b3589953 100644
--- a/client/src/components/_common/errorBoundary/AsyncBoundary.tsx
+++ b/client/src/components/_common/errorBoundary/AsyncBoundary.tsx
@@ -1,21 +1,41 @@
+import { ErrorBoundarySharedProps } from '@/myTypes/_common/errorBoundary';
import { PropsWithChildren, Suspense } from 'react';
import Spinner from '../spinner/Spinner';
-import CriticalErrorBoundary from './CriticalErrorBoundary';
-import NotFoundErrorBoundary from './NotFoundErrorBoundary';
+import { ApiErrorBoundary } from './ApiErrorBoundary';
+import { BaseErrorBoundary } from './BaseErrorBoundary';
+import { APIError } from './errors';
+import { NetworkErrorBoundary } from './NetworkErrorBoundary';
import RuntimeErrorBoundary from './RuntimeErrorBoundary';
-import ServerErrorBoundary from './ServerErrorBoundary';
-const AsyncBoundary = ({ children }: PropsWithChildren) => {
+interface AsyncBoundaryProps {
+ isCritical?: boolean;
+ onError?: ErrorBoundarySharedProps['onError'];
+ onReset?: ErrorBoundarySharedProps['onReset'];
+ resetKeys?: unknown[];
+}
+
+const AsyncBoundary = ({
+ children,
+ isCritical,
+ onError,
+ onReset,
+ resetKeys,
+}: PropsWithChildren) => {
return (
-
-
-
-
+
+ >
+
+
}>{children}
-
-
-
-
+
+
+
+
);
};
diff --git a/client/src/components/_common/errorBoundary/BaseErrorBoundary.tsx b/client/src/components/_common/errorBoundary/BaseErrorBoundary.tsx
new file mode 100644
index 000000000..bc6654b5e
--- /dev/null
+++ b/client/src/components/_common/errorBoundary/BaseErrorBoundary.tsx
@@ -0,0 +1,22 @@
+import { CkError } from './errors';
+import ErrorBoundary from './ErrorBoundary';
+import { ErrorInfo } from 'react';
+
+export class BaseErrorBoundary extends ErrorBoundary {
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
+ this.props.onError?.(error, errorInfo);
+
+ throw CkError.convertError(error);
+ }
+
+ render() {
+ const { children } = this.props;
+ const { didCatch } = this.state;
+
+ if (didCatch) {
+ return null;
+ }
+
+ return children;
+ }
+}
diff --git a/client/src/components/_common/errorBoundary/CriticalErrorBoundary.tsx b/client/src/components/_common/errorBoundary/CriticalErrorBoundary.tsx
index 68ef39e26..381018c07 100644
--- a/client/src/components/_common/errorBoundary/CriticalErrorBoundary.tsx
+++ b/client/src/components/_common/errorBoundary/CriticalErrorBoundary.tsx
@@ -1,18 +1,24 @@
import { ErrorInfo } from 'react';
-import { Critical } from '../error/ErrorComponents';
+import { Critical } from '../fallback/Fallback';
import ErrorBoundary from './ErrorBoundary';
-class CriticalErrorBoundary extends ErrorBoundary {
- componentDidCatch(_error: any, _errorInfo: ErrorInfo): void {}
+export class CriticalErrorBoundary extends ErrorBoundary {
+ static getDerivedStateFromError(error: Error): { didCatch: boolean; error: Error } {
+ return { didCatch: true, error };
+ }
+
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
+ this.props.onError?.(error, errorInfo);
+ this.setState({ fallback: });
+ }
render() {
- const { didCatch } = this.state;
- const { children } = this.props;
+ const { didCatch, fallback: innerFallback } = this.state;
+ const { children, fallback: custumFallback } = this.props;
+
if (didCatch) {
- return ;
+ return custumFallback ?? innerFallback ?? critical error
;
}
return children;
}
}
-
-export default CriticalErrorBoundary;
diff --git a/client/src/components/_common/errorBoundary/ErrorBoundary.tsx b/client/src/components/_common/errorBoundary/ErrorBoundary.tsx
index c3dfd35f5..43a14bf61 100644
--- a/client/src/components/_common/errorBoundary/ErrorBoundary.tsx
+++ b/client/src/components/_common/errorBoundary/ErrorBoundary.tsx
@@ -1,13 +1,20 @@
import { ErrorBoundaryProps, ErrorBoundaryState } from '@/myTypes/_common/errorBoundary';
-import { Component } from 'react';
+import { Component, PropsWithChildren } from 'react';
+
+function hasArrayChanged(a: any[] = [], b: any[] = []) {
+ return a.length !== b.length || a.some((item, index) => !Object.is(item, b[index]));
+}
const initialState: ErrorBoundaryState = {
didCatch: false,
error: null,
};
-class ErrorBoundary extends Component {
- constructor(props: ErrorBoundaryProps) {
+class ErrorBoundary extends Component<
+ PropsWithChildren,
+ ErrorBoundaryState
+> {
+ constructor(props: PropsWithChildren) {
super(props);
this.state = initialState;
@@ -17,8 +24,34 @@ class ErrorBoundary extends Component {
return { didCatch: true, error };
}
- resetError() {
- if (this.state.didCatch) {
+ componentDidUpdate(prevProps: ErrorBoundaryProps, prevState: ErrorBoundaryState) {
+ const { didCatch } = this.state;
+ const { resetKeys } = this.props;
+
+ if (
+ didCatch &&
+ prevState.error !== null &&
+ hasArrayChanged(prevProps.resetKeys, resetKeys)
+ ) {
+ this.props.onReset?.({
+ next: resetKeys,
+ prev: prevProps.resetKeys,
+ reason: 'keys',
+ });
+
+ this.setState(initialState);
+ }
+ }
+
+ resetErrorBoundary(...args: unknown[]) {
+ const { error } = this.state;
+
+ if (error !== null) {
+ this.props.onReset?.({
+ args,
+ reason: 'keys',
+ });
+
this.setState(initialState);
}
}
diff --git a/client/src/components/_common/errorBoundary/NetworkErrorBoundary.tsx b/client/src/components/_common/errorBoundary/NetworkErrorBoundary.tsx
new file mode 100644
index 000000000..4b3296427
--- /dev/null
+++ b/client/src/components/_common/errorBoundary/NetworkErrorBoundary.tsx
@@ -0,0 +1,29 @@
+import { ErrorInfo } from 'react';
+import { Offline } from '../fallback/Fallback';
+import ErrorBoundary from './ErrorBoundary';
+import { NetworkError } from './errors';
+
+export class NetworkErrorBoundary extends ErrorBoundary {
+ static getDerivedStateFromError(error: Error): { didCatch: boolean; error: Error } {
+ if (error instanceof NetworkError) {
+ return { didCatch: true, error };
+ }
+ throw error;
+ }
+
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
+ if (this.props.isCritical) throw error;
+
+ this.props.onError?.(error, errorInfo);
+ this.setState({ fallback: });
+ }
+
+ render() {
+ const { didCatch, fallback: innerFallback } = this.state;
+ const { children, fallback: customFllback } = this.props;
+ if (didCatch) {
+ return customFllback ?? innerFallback ?? network error!
;
+ }
+ return children;
+ }
+}
diff --git a/client/src/components/_common/errorBoundary/NotFoundErrorBoundary.tsx b/client/src/components/_common/errorBoundary/NotFoundErrorBoundary.tsx
deleted file mode 100644
index 71959d4ba..000000000
--- a/client/src/components/_common/errorBoundary/NotFoundErrorBoundary.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { ErrorInfo } from 'react';
-import { NotFound } from '../error/ErrorComponents';
-import ErrorBoundary from './ErrorBoundary';
-
-class NotFoundErrorBoundary extends ErrorBoundary {
- componentDidCatch(error: any, _errorInfo: ErrorInfo): void {
- if (error.response.status !== 404) throw error;
- }
-
- render() {
- const { didCatch } = this.state;
- const { children } = this.props;
- if (didCatch) {
- return ;
- }
- return children;
- }
-}
-
-export default NotFoundErrorBoundary;
diff --git a/client/src/components/_common/errorBoundary/RuntimeErrorBoundary.tsx b/client/src/components/_common/errorBoundary/RuntimeErrorBoundary.tsx
index 55b57986b..390fd381e 100644
--- a/client/src/components/_common/errorBoundary/RuntimeErrorBoundary.tsx
+++ b/client/src/components/_common/errorBoundary/RuntimeErrorBoundary.tsx
@@ -3,13 +3,22 @@ import { Runtime } from '../error/ErrorComponents';
import ErrorBoundary from './ErrorBoundary';
class RuntimeErrorBoundary extends ErrorBoundary {
- componentDidCatch(_error: any, _errorInfo: ErrorInfo): void {}
+ static getDerivedStateFromError(error: Error): { didCatch: boolean; error: Error } {
+ return { didCatch: true, error };
+ }
+
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
+ if (this.props.isCritical) throw error;
+
+ this.props.onError?.(error, errorInfo);
+ }
render() {
const { didCatch } = this.state;
- const { children } = this.props;
+ const { children, fallback: customFallback } = this.props;
+
if (didCatch) {
- return ;
+ return customFallback ?? ;
}
return children;
}
diff --git a/client/src/components/_common/errorBoundary/ServerErrorBoundary.tsx b/client/src/components/_common/errorBoundary/ServerErrorBoundary.tsx
deleted file mode 100644
index 674a8c6ac..000000000
--- a/client/src/components/_common/errorBoundary/ServerErrorBoundary.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { SERVER_ERROR_CODE } from '@/constants/_common/regex';
-import { ErrorInfo } from 'react';
-import { ServerError } from '../error/ErrorComponents';
-import ErrorBoundary from './ErrorBoundary';
-
-class ServerErrorBoundary extends ErrorBoundary {
- componentDidCatch(error: any, _errorInfo: ErrorInfo): void {
- if (!SERVER_ERROR_CODE.test(error.response.status)) throw error;
- }
-
- render() {
- const { didCatch } = this.state;
- const { children } = this.props;
- if (didCatch) {
- return ;
- }
- return children;
- }
-}
-
-export default ServerErrorBoundary;
diff --git a/client/src/components/_common/errorBoundary/errors.ts b/client/src/components/_common/errorBoundary/errors.ts
new file mode 100644
index 000000000..28c653fe2
--- /dev/null
+++ b/client/src/components/_common/errorBoundary/errors.ts
@@ -0,0 +1,84 @@
+/* eslint-disable @typescript-eslint/no-use-before-define */
+// eslint-disable-next-line max-classes-per-file
+import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
+
+type AxiosProperitesType =
+ | 'code'
+ | 'config'
+ | 'message'
+ | 'name'
+ | 'request'
+ | 'response';
+
+type RequiredForAPIErrorType = Pick;
+type AxiosErrorsRequiredType = {
+ [k in keyof RequiredForAPIErrorType]-?: RequiredForAPIErrorType[k];
+} & Omit;
+
+function validateRequiredAxiosErrorType(
+ error: AxiosError
+): error is AxiosErrorsRequiredType> {
+ const axiosProperites: AxiosProperitesType[] = [
+ 'code',
+ 'config',
+ 'message',
+ 'name',
+ 'request',
+ 'response',
+ ];
+
+ return axiosProperites.every((property) => error[property] !== undefined);
+}
+
+export class CkError extends Error {
+ constructor(error: T) {
+ super(error.message, { cause: error.cause });
+ }
+
+ static checkIsNetworkError() {
+ return window.navigator.onLine === false;
+ }
+
+ static convertError(error: unknown) {
+ if (error instanceof AxiosError && error?.response?.status !== 500) {
+ const isApiError = validateRequiredAxiosErrorType(error);
+
+ return isApiError ? new APIError(error) : new RuntimeError(error);
+ }
+
+ if (this.checkIsNetworkError()) {
+ return new NetworkError(error as Error);
+ }
+
+ return new RuntimeError(error as Error);
+ }
+}
+
+export class APIError extends CkError> {
+ errorCode: string;
+
+ errorConfig: AxiosRequestConfig;
+
+ errorMessage: string;
+
+ errorName: string;
+
+ request: XMLHttpRequest;
+
+ response: AxiosResponse;
+
+ constructor(error: AxiosErrorsRequiredType) {
+ super(error);
+
+ this.errorCode = error.code;
+ this.errorConfig = error.config;
+ this.errorMessage = error.message;
+ this.errorName = error.name;
+ this.request = error.request;
+ this.response = error.response;
+ }
+}
+
+export class RuntimeError extends CkError {}
+
+export class NetworkError extends CkError {}
diff --git a/client/src/components/_common/fallback/Fallback.tsx b/client/src/components/_common/fallback/Fallback.tsx
index 029fc0ae8..e23b41388 100644
--- a/client/src/components/_common/fallback/Fallback.tsx
+++ b/client/src/components/_common/fallback/Fallback.tsx
@@ -1,5 +1,87 @@
-const Fallback = () => {
- return Loading...
;
+import * as S from './fallback.styles';
+import elephantImage from '../../../assets/images/cryingelephant.png';
+import { useNavigate } from 'react-router-dom';
+
+export const NotFound = () => {
+ const navigate = useNavigate();
+
+ const moveMainPage = () => {
+ navigate('/');
+ window.location.reload();
+ };
+ return (
+
+
+ 404 Not Found
+ 잘못된 경로 접근했어요
+ 메인페이지로 돌아가기
+
+ );
+};
+
+export const Forbidden = () => {
+ const navigate = useNavigate();
+
+ const moveMainPage = () => {
+ navigate('/');
+ window.location.reload();
+ };
+ return (
+
+
+ 403 Forbidden
+ 접근할 수 없는 페이지입니다
+ 메인페이지로 돌아가기
+
+ );
+};
+
+export const Runtime = () => {
+ const navigate = useNavigate();
+
+ const moveMainPage = () => {
+ navigate('/');
+ window.location.reload();
+ };
+ return (
+
+
+ Error
+
+ 죄송합니다, 페이지를 로드하는 동안 오류가 발생했습니다
+
+ 메인페이지로 돌아가기
+
+ );
+};
+
+export const Critical = () => {
+ return (
+
+
+ Service Not Working
+
+ 죄송합니다, 알 수 없는 이유로 서비스 사용이 불가능합니다 😭
+ 다음에 다시 이용해주세요.
+
+ 불편사항 문의하기
+
+ );
};
-export default Fallback;
+export const Offline = () => {
+ const navigate = useNavigate();
+
+ const movePrevPage = () => {
+ navigate(-1);
+ };
+ return (
+
+ 네트워크 상태가 불안정합니다.
+ window.location.reload()}>
+ 다시 시도하기
+
+ 이전 페이지로 돌아가기
+
+ );
+};
diff --git a/client/src/components/_common/fallback/fallback.styles.ts b/client/src/components/_common/fallback/fallback.styles.ts
new file mode 100644
index 000000000..f6ab1e059
--- /dev/null
+++ b/client/src/components/_common/fallback/fallback.styles.ts
@@ -0,0 +1,70 @@
+import styled from 'styled-components';
+
+export const Container = styled.section`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ width: 100%;
+ height: 100vh;
+`;
+
+export const ElephantImage = styled.img`
+ width: 20%;
+`;
+
+export const NotFoundTitle = styled.h1`
+ ${({ theme }) => theme.fonts.title_large};
+ font-size: 4rem;
+ color: ${({ theme }) => theme.colors.main_dark};
+`;
+
+export const NotFoundText = styled.h2`
+ ${({ theme }) => theme.fonts.h2};
+ color: ${({ theme }) => theme.colors.gray300};
+`;
+
+export const ForbiddenTitle = styled.h1`
+ ${({ theme }) => theme.fonts.title_large};
+ font-size: 4rem;
+ color: ${({ theme }) => theme.colors.main_dark};
+`;
+
+export const ForbiddenText = styled.h2`
+ ${({ theme }) => theme.fonts.h2};
+ color: ${({ theme }) => theme.colors.gray300};
+`;
+
+export const RuntimeTitle = styled.h1`
+ ${({ theme }) => theme.fonts.title_large};
+ font-size: 4rem;
+ color: ${({ theme }) => theme.colors.main_dark};
+`;
+
+export const RuntimeText = styled.h2`
+ ${({ theme }) => theme.fonts.h2};
+ color: ${({ theme }) => theme.colors.gray300};
+`;
+
+export const CriticalTitle = styled.h1`
+ ${({ theme }) => theme.fonts.title_large};
+ font-size: 4rem;
+ color: ${({ theme }) => theme.colors.main_dark};
+`;
+
+export const CriticalText = styled.h2`
+ ${({ theme }) => theme.fonts.h2};
+ color: ${({ theme }) => theme.colors.gray300};
+`;
+
+export const MovePageButton = styled.button`
+ ${({ theme }) => theme.fonts.button1}
+ margin: 2rem 0;
+ padding: 1rem 2rem;
+
+ color: ${({ theme }) => theme.colors.white};
+
+ background-color: ${({ theme }) => theme.colors.main_middle};
+ border-radius: 30px;
+`;
diff --git a/client/src/components/_common/privateRouter/PrivateRouter.tsx b/client/src/components/_common/privateRouter/PrivateRouter.tsx
index c02e0323d..88fd2500c 100644
--- a/client/src/components/_common/privateRouter/PrivateRouter.tsx
+++ b/client/src/components/_common/privateRouter/PrivateRouter.tsx
@@ -3,6 +3,7 @@ import { useUserInfoContext } from '@components/_providers/UserInfoProvider';
import useToast from '@hooks/_common/useToast';
import { useNavigate } from 'react-router-dom';
import { getCookie } from '@utils/_common/cookies';
+import SVGIcon from '@/components/icons/SVGIcon';
const PrivateRouter = (props: PropsWithChildren) => {
const { children } = props;
@@ -14,7 +15,11 @@ const PrivateRouter = (props: PropsWithChildren) => {
useEffect(() => {
if (userInfo.id === null && !accessToken) {
navigate('/login');
- triggerToast({ message: '로그인이 필요한 서비스입니다.' });
+ triggerToast({
+ message: '로그인이 필요한 서비스입니다.',
+ indicator: ,
+ isError: true,
+ });
}
}, [userInfo.id, navigate]);
diff --git a/client/src/components/_common/sessionHandler/SessionHandler.tsx b/client/src/components/_common/sessionHandler/SessionHandler.tsx
index 8489d8583..388de7c93 100644
--- a/client/src/components/_common/sessionHandler/SessionHandler.tsx
+++ b/client/src/components/_common/sessionHandler/SessionHandler.tsx
@@ -6,6 +6,7 @@ import {
useUserInfoContext,
} from '@components/_providers/UserInfoProvider';
import { deleteCookie } from '@utils/_common/cookies';
+import SVGIcon from '@/components/icons/SVGIcon';
const SessionHandler = (props: PropsWithChildren) => {
const navigate = useNavigate();
@@ -17,7 +18,11 @@ const SessionHandler = (props: PropsWithChildren) => {
deleteCookie('access_token');
deleteCookie('refresh_token');
setUserInfo(defaultUserInfo);
- triggerToast({ message: '세션이 만료되었습니다. 재로그인 해주세요.' });
+ triggerToast({
+ message: '세션이 만료되었습니다. 재로그인 해주세요.',
+ indicator: ,
+ isError: true,
+ });
navigate('/login');
};
diff --git a/client/src/components/_common/toastProvider/ToastProvider.styles.ts b/client/src/components/_common/toast/Toast.styles.ts
similarity index 88%
rename from client/src/components/_common/toastProvider/ToastProvider.styles.ts
rename to client/src/components/_common/toast/Toast.styles.ts
index 4dcc10bb9..f8744b445 100644
--- a/client/src/components/_common/toastProvider/ToastProvider.styles.ts
+++ b/client/src/components/_common/toast/Toast.styles.ts
@@ -5,33 +5,33 @@ type ToastContainerProps = {
};
export const ToastContainer = styled.div`
- ${({ theme }) => theme.fonts.description5}
+ ${({ theme }) => theme.fonts.description3}
position: fixed;
right: 0;
bottom: 0;
display: flex;
align-items: center;
- justify-content: center;
+ justify-content: flex-start;
- width: 20%;
+ width: 25%;
min-width: 15rem;
max-width: 50rem;
margin: 1rem;
- padding: 0.5rem;
+ padding: 1.5rem;
- color: ${(props) =>
- props.isError ? props.theme.colors.white : props.theme.colors.black};
+ color: ${(props) => props.theme.colors.white};
background-color: ${(props) =>
- props.isError ? props.theme.colors.red : props.theme.colors.main_light};
- border-radius: 10px;
+ props.isError ? '#F16666' : props.theme.colors.main_dark};
+ border-radius: 12px;
box-shadow: ${({ theme }) => theme.shadows.box};
`;
export const ToastMessageContainer = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
+ padding-left: 2rem;
`;
export const ToastMessage = styled.span`
diff --git a/client/src/components/_common/toastProvider/ToastProvider.tsx b/client/src/components/_common/toast/Toast.tsx
similarity index 78%
rename from client/src/components/_common/toastProvider/ToastProvider.tsx
rename to client/src/components/_common/toast/Toast.tsx
index 6dda132a0..483fd5afa 100644
--- a/client/src/components/_common/toastProvider/ToastProvider.tsx
+++ b/client/src/components/_common/toast/Toast.tsx
@@ -1,45 +1,43 @@
-import { createContext, PropsWithChildren, useRef, useState } from 'react';
+import { PropsWithChildren, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
-import SVGIcon from '@components/icons/SVGIcon';
-import * as S from './ToastProvider.styles';
-import { ToastContainerProps, ToastContextType } from '@myTypes/_common/toast';
+import * as S from './Toast.styles';
+import { ToastContainerProps } from '@myTypes/_common/toast';
+import { ToastContext } from '@/context/toastContext';
export const ToastMessage = ({
- isError = false,
message,
+ indicator,
+ isError = false,
onClickToast,
}: ToastContainerProps) => {
return (
+ {indicator}
{message}
-
);
};
-export const ToastContext = createContext({
- triggerToast: () => {},
-});
-
const ToastProvider = (props: PropsWithChildren) => {
const { children } = props;
const timeout = useRef(null);
- const [{ message, isError, isShow }, setMessage] = useState<
+ const [{ message, indicator, isError, isShow }, setMessage] = useState<
Omit
>({
message: '',
+ indicator: null,
isError: false,
isShow: null,
});
- const triggerToast = ({ message, isError = false }: ToastContainerProps) => {
+ const triggerToast = ({ message, indicator, isError = false }: ToastContainerProps) => {
if (!timeout.current) {
- setMessage((prev) => ({ ...prev, isError, message, isShow: true }));
+ setMessage((prev) => ({ ...prev, isError, message, indicator, isShow: true }));
timeout.current = setTimeout(() => {
setMessage((prev) => ({ ...prev, isShow: false }));
@@ -87,6 +85,7 @@ const ToastProvider = (props: PropsWithChildren) => {
@@ -96,6 +95,7 @@ const ToastProvider = (props: PropsWithChildren) => {
diff --git a/client/src/components/goalRoomDahsboardPage/goalRoomCertificationFeed/GoalRoomCertificationFeed.tsx b/client/src/components/goalRoomDahsboardPage/goalRoomCertificationFeed/GoalRoomCertificationFeed.tsx
index a29ac4ffd..69e462ccf 100644
--- a/client/src/components/goalRoomDahsboardPage/goalRoomCertificationFeed/GoalRoomCertificationFeed.tsx
+++ b/client/src/components/goalRoomDahsboardPage/goalRoomCertificationFeed/GoalRoomCertificationFeed.tsx
@@ -1,25 +1,19 @@
import * as S from './GoalRoomCertificationFeed.styles';
import SVGIcon from '@components/icons/SVGIcon';
-import { GoalRoomBrowseResponse } from '@myTypes/goalRoom/remote';
import { StyledImage } from './GoalRoomCertificationFeed.styles';
-import {
- DialogBackdrop,
- DialogBox,
- DialogContent,
- DialogTrigger,
-} from '@components/_common/dialog/dialog';
+import { Dialog } from 'ck-util-components';
import CertificationFeedModal from '@components/goalRoomDahsboardPage/goalRoomCertificationFeed/certificationFeedModal/CertificationFeedModal';
import ToolTip from '@components/_common/toolTip/ToolTip';
+import { useGoalRoomDashboardContext } from '@/context/goalRoomDashboardContext';
+import { useFetchGoalRoom } from '@/hooks/queries/goalRoom';
-type GoalRoomCertificationFeedProps = {
- goalRoomData: GoalRoomBrowseResponse;
-};
-
-const GoalRoomCertificationFeed = ({ goalRoomData }: GoalRoomCertificationFeedProps) => {
- const { checkFeeds } = goalRoomData;
+const GoalRoomCertificationFeed = () => {
+ const { goalroomId } = useGoalRoomDashboardContext();
+ const { goalRoom } = useFetchGoalRoom(goalroomId);
+ const { checkFeeds } = goalRoom;
return (
-
+
+
+
);
};
diff --git a/client/src/components/goalRoomDahsboardPage/goalRoomDahsboardRoadmap/GoalRoomDashboardRoadmap.tsx b/client/src/components/goalRoomDahsboardPage/goalRoomDahsboardRoadmap/GoalRoomDashboardRoadmap.tsx
index c715da4dd..338124aab 100644
--- a/client/src/components/goalRoomDahsboardPage/goalRoomDahsboardRoadmap/GoalRoomDashboardRoadmap.tsx
+++ b/client/src/components/goalRoomDahsboardPage/goalRoomDahsboardRoadmap/GoalRoomDashboardRoadmap.tsx
@@ -1,38 +1,29 @@
import * as S from './GoalRoomDashboardRoadmap.styles';
-import { useGoalRoomDetail } from '@/hooks/queries/goalRoom';
+import { useFetchGoalRoom, useGoalRoomDetail } from '@/hooks/queries/goalRoom';
import { useGoalRoomDashboardContext } from '@/context/goalRoomDashboardContext';
-import {
- DialogBackdrop,
- DialogBox,
- DialogContent,
- DialogTrigger,
-} from '@components/_common/dialog/dialog';
+import { Dialog } from 'ck-util-components';
import SVGIcon from '@components/icons/SVGIcon';
import RoadmapModal from '@components/goalRoomDahsboardPage/goalRoomDahsboardRoadmap/roadmapModal/RoadmapModal';
-import { GoalRoomRecruitmentStatus } from '@myTypes/goalRoom/internal';
-type GoalRoomDashboardRoadmapProps = {
- goalRoomStatus: GoalRoomRecruitmentStatus;
-};
-
-const GoalRoomDashboardRoadmap = ({ goalRoomStatus }: GoalRoomDashboardRoadmapProps) => {
+const GoalRoomDashboardRoadmap = () => {
const { goalroomId } = useGoalRoomDashboardContext();
+ const { goalRoom } = useFetchGoalRoom(goalroomId);
const { goalRoomInfo } = useGoalRoomDetail(Number(goalroomId));
return (
-
+
+
+ <>{goalRoom.status === 'RUNNING' && }>
+
+
);
};
diff --git a/client/src/components/goalRoomDahsboardPage/goalRoomDahsboardTodo/GoalRoomDashboardTodo.tsx b/client/src/components/goalRoomDahsboardPage/goalRoomDahsboardTodo/GoalRoomDashboardTodo.tsx
index 3c3e09976..8aa5cc45d 100644
--- a/client/src/components/goalRoomDahsboardPage/goalRoomDahsboardTodo/GoalRoomDashboardTodo.tsx
+++ b/client/src/components/goalRoomDahsboardPage/goalRoomDahsboardTodo/GoalRoomDashboardTodo.tsx
@@ -1,29 +1,23 @@
import * as S from './GoalRoomDashboardTodo.styles';
import SVGIcon from '@components/icons/SVGIcon';
-import { GoalRoomBrowseResponse } from '@myTypes/goalRoom/remote';
import SingleTodo from '@components/goalRoomDahsboardPage/goalRoomDahsboardTodo/singleTodo/SingleTodo';
-import {
- DialogBackdrop,
- DialogBox,
- DialogContent,
- DialogTrigger,
-} from '@components/_common/dialog/dialog';
+import { Dialog } from 'ck-util-components';
import TodoModal from '@components/goalRoomDahsboardPage/goalRoomDahsboardTodo/todoModal/TodoModal';
import ToolTip from '@components/_common/toolTip/ToolTip';
+import { useGoalRoomDashboardContext } from '@/context/goalRoomDashboardContext';
+import { useFetchGoalRoom } from '@/hooks/queries/goalRoom';
type GoalRoomDashboardTodoProps = {
- goalRoomData: GoalRoomBrowseResponse;
isLeader: boolean;
};
-const GoalRoomDashboardTodo = ({
- goalRoomData,
- isLeader,
-}: GoalRoomDashboardTodoProps) => {
- const { goalRoomTodos } = goalRoomData;
+const GoalRoomDashboardTodo = ({ isLeader }: GoalRoomDashboardTodoProps) => {
+ const { goalroomId } = useGoalRoomDashboardContext();
+ const { goalRoom } = useFetchGoalRoom(goalroomId);
+ const { goalRoomTodos } = goalRoom;
return (
-
+
+
+
);
};
diff --git a/client/src/components/goalRoomDahsboardPage/goalRoomDashboardContent/GoalRoomDashboardContent.tsx b/client/src/components/goalRoomDahsboardPage/goalRoomDashboardContent/GoalRoomDashboardContent.tsx
index 0ae81b7ba..46b71c53b 100644
--- a/client/src/components/goalRoomDahsboardPage/goalRoomDashboardContent/GoalRoomDashboardContent.tsx
+++ b/client/src/components/goalRoomDahsboardPage/goalRoomDashboardContent/GoalRoomDashboardContent.tsx
@@ -26,27 +26,25 @@ const GoalRoomDashboardContent = () => {
const isLeader = userInfo.id === goalRoom.leaderId;
return (
- <>
- }>
-
-
-
-
-
-
-
-
- {goalRoom.status !== 'RUNNING' && (
-
- )}
-
-
- >
+ }>
+
+
+
+
+
+
+
+
+ {goalRoom.status !== 'RUNNING' && (
+
+ )}
+
+
);
};
diff --git a/client/src/components/goalRoomDahsboardPage/goalRoomDashboardHeader/GoalRoomDashboardHeader.tsx b/client/src/components/goalRoomDahsboardPage/goalRoomDashboardHeader/GoalRoomDashboardHeader.tsx
index e11753bee..6f3d19528 100644
--- a/client/src/components/goalRoomDahsboardPage/goalRoomDashboardHeader/GoalRoomDashboardHeader.tsx
+++ b/client/src/components/goalRoomDahsboardPage/goalRoomDashboardHeader/GoalRoomDashboardHeader.tsx
@@ -1,32 +1,21 @@
import SVGIcon from '@components/icons/SVGIcon';
-
import * as S from './GoalRoomDashboardHeader.styles';
import recruitmentStatus from '@constants/goalRoom/recruitmentStatus';
-import { GoalRoomBrowseResponse } from '@myTypes/goalRoom/remote';
-import {
- DialogBackdrop,
- DialogBox,
- DialogContent,
- DialogTrigger,
-} from '@components/_common/dialog/dialog';
+import { Dialog } from 'ck-util-components';
import GoalRoomParticipantsListModal from '@components/goalRoomDahsboardPage/goalRoomDashboardHeader/goalRoomParticipantsListModal/GoalRoomParticipantsListModal';
import isTodayOrAfter from '@utils/_common/isTodayOrAfter';
import { useGoalRoomDashboardContext } from '@/context/goalRoomDashboardContext';
-import { useStartGoalRoom } from '@hooks/queries/goalRoom';
+import { useFetchGoalRoom, useStartGoalRoom } from '@hooks/queries/goalRoom';
type GoalRoomDashboardHeaderProps = {
- goalRoomData: GoalRoomBrowseResponse;
isLeader: boolean;
};
-const GoalRoomDashboardHeader = ({
- goalRoomData,
- isLeader,
-}: GoalRoomDashboardHeaderProps) => {
- const { name, status, currentMemberCount, limitedMemberCount, startDate, endDate } =
- goalRoomData;
-
+const GoalRoomDashboardHeader = ({ isLeader }: GoalRoomDashboardHeaderProps) => {
const { goalroomId } = useGoalRoomDashboardContext();
+ const { goalRoom } = useFetchGoalRoom(goalroomId);
+ const { name, status, currentMemberCount, limitedMemberCount, startDate, endDate } =
+ goalRoom;
const { startGoalRoom } = useStartGoalRoom(goalroomId);
@@ -38,7 +27,7 @@ const GoalRoomDashboardHeader = ({
};
return (
-
+
+
+
);
};
diff --git a/client/src/components/goalRoomDahsboardPage/goalRoomUserRanking/GoalRoomUserRanking.tsx b/client/src/components/goalRoomDahsboardPage/goalRoomUserRanking/GoalRoomUserRanking.tsx
index 7b842feaa..3ba23b0e8 100644
--- a/client/src/components/goalRoomDahsboardPage/goalRoomUserRanking/GoalRoomUserRanking.tsx
+++ b/client/src/components/goalRoomDahsboardPage/goalRoomUserRanking/GoalRoomUserRanking.tsx
@@ -5,12 +5,7 @@ import { GoalRoomDashboardContentParams } from '@components/goalRoomDahsboardPag
import podiumImg from '@assets/images/podium.png';
import podiumImgAV from '@assets/images/podium.avif';
import { useUserInfoContext } from '@components/_providers/UserInfoProvider';
-import {
- DialogBackdrop,
- DialogBox,
- DialogContent,
- DialogTrigger,
-} from '@components/_common/dialog/dialog';
+import { Dialog } from 'ck-util-components';
import SVGIcon from '@components/icons/SVGIcon';
import GoalRoomRankingModal from '@components/goalRoomDahsboardPage/goalRoomUserRanking/goalRoomRankingModal/GoalRoomRankingModal';
import ToolTip from '@components/_common/toolTip/ToolTip';
@@ -33,7 +28,7 @@ const GoalRoomUserRanking = () => {
) + 1;
return (
-
+
-
+
-
-
+
+
-
-
+
+
);
};
diff --git a/client/src/components/goalRoomListPage/goalRoomDetail/GoalRoomDetailDialog.tsx b/client/src/components/goalRoomListPage/goalRoomDetail/GoalRoomDetailDialog.tsx
index 57ff00346..2c997c597 100644
--- a/client/src/components/goalRoomListPage/goalRoomDetail/GoalRoomDetailDialog.tsx
+++ b/client/src/components/goalRoomListPage/goalRoomDetail/GoalRoomDetailDialog.tsx
@@ -1,9 +1,4 @@
-import {
- DialogBackdrop,
- DialogBox,
- DialogContent,
- DialogTrigger,
-} from '@components/_common/dialog/dialog';
+import { Dialog } from 'ck-util-components';
import * as S from './goalRoomDetailDialog.styles';
import GoalRoomDetailDialogContent from './GoalRoomDetailDialogContent';
@@ -13,17 +8,17 @@ type GoalRoomDetailDialogProps = {
const GoalRoomDetailDialog = ({ goalRoomId }: GoalRoomDetailDialogProps) => {
return (
-
-
+
-
+
+
-
-
+
+
-
-
+
+
);
};
diff --git a/client/src/components/goalRoomListPage/goalRoomDetail/GoalRoomDetailDialogContent.tsx b/client/src/components/goalRoomListPage/goalRoomDetail/GoalRoomDetailDialogContent.tsx
index ccaaedbb5..a4d889384 100644
--- a/client/src/components/goalRoomListPage/goalRoomDetail/GoalRoomDetailDialogContent.tsx
+++ b/client/src/components/goalRoomListPage/goalRoomDetail/GoalRoomDetailDialogContent.tsx
@@ -1,4 +1,4 @@
-import { DialogTrigger } from '@/components/_common/dialog/dialog';
+import { Dialog } from 'ck-util-components';
import { useGoalRoomDetail, useJoinGoalRoom } from '@hooks/queries/goalRoom';
import { Link } from 'react-router-dom';
import * as S from './goalRoomDetailDialog.styles';
@@ -19,9 +19,9 @@ const GoalRoomDetailDialogContent = ({
{goalRoomInfo.name}
-
+
X
-
+
{goalRoomInfo.currentMemberCount}
/{goalRoomInfo.limitedMemberCount}
diff --git a/client/src/components/goalRoomListPage/goalRoomList/GoalRoomFilter.tsx b/client/src/components/goalRoomListPage/goalRoomList/GoalRoomFilter.tsx
index 1a5876466..e9f47b266 100644
--- a/client/src/components/goalRoomListPage/goalRoomList/GoalRoomFilter.tsx
+++ b/client/src/components/goalRoomListPage/goalRoomList/GoalRoomFilter.tsx
@@ -1,33 +1,47 @@
-import { Select } from '@/components/roadmapCreatePage/selector/SelectBox';
+import { Select } from 'ck-util-components';
import { goalRoomFilter } from '@/constants/goalRoom/goalRoomFilter';
-import React, { useState } from 'react';
+import { useState } from 'react';
import * as S from './goalRoomList.styles';
+import { getInvariantObjectKeys, invariantOf } from '@/utils/_common/invariantType';
-const GoalRoomFilter = ({
- children,
-}: {
- children: (
- selectedOption: (typeof goalRoomFilter)[keyof typeof goalRoomFilter]
- ) => React.ReactNode;
-}) => {
- const [selectedOption, setSelectedOption] = useState<
- (typeof goalRoomFilter)[keyof typeof goalRoomFilter]
- >(goalRoomFilter['1']);
+interface GoalRoomFilterProps {
+ sortedOption: (typeof goalRoomFilter)[keyof typeof goalRoomFilter];
+ selectOption: (option: keyof typeof goalRoomFilter) => void;
+}
- const selectFilterOption = (id: number) => {
- // eslint-disable-next-line no-prototype-builtins
- if (goalRoomFilter.hasOwnProperty(id)) {
- setSelectedOption(goalRoomFilter[id as keyof typeof goalRoomFilter]);
+const GoalRoomFilter = ({ sortedOption, selectOption }: GoalRoomFilterProps) => {
+ const [filterOpen, setFilterOpen] = useState(false);
+
+ const toggleFilter = () => {
+ if (filterOpen) {
+ setFilterOpen(false);
+ } else {
+ setFilterOpen(true);
}
};
return (
-
);
diff --git a/client/src/components/goalRoomListPage/goalRoomList/GoalRoomList.tsx b/client/src/components/goalRoomListPage/goalRoomList/GoalRoomList.tsx
index 8685f874d..e5599c83d 100644
--- a/client/src/components/goalRoomListPage/goalRoomList/GoalRoomList.tsx
+++ b/client/src/components/goalRoomListPage/goalRoomList/GoalRoomList.tsx
@@ -2,19 +2,19 @@ import * as S from './goalRoomList.styles';
import GoalRoomItem from './GoalRoomItem';
import { useGoalRoomList } from '@/hooks/queries/goalRoom';
import useValidParams from '@/hooks/_common/useValidParams';
-import GoalRoomFilter from './GoalRoomFilter';
-import { Select } from '@/components/roadmapCreatePage/selector/SelectBox';
import { useState } from 'react';
import { FILTER_COND, goalRoomFilter } from '@/constants/goalRoom/goalRoomFilter';
import { useInfiniteScroll } from '@hooks/_common/useInfiniteScroll';
import WavyLoading from '@components/_common/wavyLoading/WavyLoading';
import { Link } from 'react-router-dom';
+import GoalRoomFilter from './GoalRoomFilter';
const GoalRoomList = () => {
const { id } = useValidParams<{ id: string }>();
const [sortedOption, setSortedOption] = useState<
(typeof goalRoomFilter)[keyof typeof goalRoomFilter]
>(goalRoomFilter['1']);
+
const {
goalRoomListResponse: { responses: goalRoomList, hasNext },
fetchNextPage,
@@ -29,6 +29,14 @@ const GoalRoomList = () => {
fetchNextPage,
});
+ const RecruitingGoalRoomList = goalRoomList.filter(
+ (goalRoomInfo) => goalRoomInfo.status === 'RECRUITING'
+ );
+
+ const selectOption = (option: keyof typeof goalRoomFilter) => {
+ setSortedOption(goalRoomFilter[option]);
+ };
+
return (
@@ -40,31 +48,20 @@ const GoalRoomList = () => {
}
개
-
- {(selectedOption) => {
- setSortedOption(selectedOption);
- return (
-
-
-
- {goalRoomFilter['1']}
-
-
- {goalRoomFilter['2']}
-
-
-
- );
- }}
-
+
-
- {goalRoomList
- .filter((goalRoomInfo) => goalRoomInfo.status === 'RECRUITING')
- .map((goalRoomInfo) => (
+ {RecruitingGoalRoomList.length ? (
+
+ {RecruitingGoalRoomList.map((goalRoomInfo) => (
))}
-
+
+ ) : (
+
+ 현재 모집중인 모임이 존재하지 않아요
+ 모임을 생성해서 목표 달성을 함께 할 동료들을 모집 해 보세요!
+
+ )}
{hasNext && }
diff --git a/client/src/components/goalRoomListPage/goalRoomList/goalRoomList.styles.ts b/client/src/components/goalRoomListPage/goalRoomList/goalRoomList.styles.ts
index 87d93d3ff..68e1e7d6f 100644
--- a/client/src/components/goalRoomListPage/goalRoomList/goalRoomList.styles.ts
+++ b/client/src/components/goalRoomListPage/goalRoomList/goalRoomList.styles.ts
@@ -144,3 +144,20 @@ export const FilterOption = styled.li`
background-color: ${({ theme }) => theme.colors.main_dark};
`;
+
+export const NoContent = styled.div`
+ ${({ theme }) => theme.fonts.h1}
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ margin-top: 8rem;
+
+ line-height: 3rem;
+
+ opacity: 0.6;
+
+ & > div:first-child {
+ margin-bottom: 2rem;
+ }
+`;
diff --git a/client/src/components/icons/svgIcons.tsx b/client/src/components/icons/svgIcons.tsx
index 1c766aa6a..6926831c6 100644
--- a/client/src/components/icons/svgIcons.tsx
+++ b/client/src/components/icons/svgIcons.tsx
@@ -2503,3 +2503,48 @@ export const NoImageIcon = ({ width, ...props }: SVGProps) => (
/>
);
+
+export const SuccessIcon = ({ width, ...props }: SVGProps) => (
+
+);
+
+export const ErrorIcon = ({ width, ...props }: SVGProps) => (
+
+);
+
+export const WarningIcon = ({ width, ...props }: SVGProps) => (
+
+);
diff --git a/client/src/components/loginPage/loginForm/LoginForm.tsx b/client/src/components/loginPage/loginForm/LoginForm.tsx
index f985ed794..6885f14a0 100644
--- a/client/src/components/loginPage/loginForm/LoginForm.tsx
+++ b/client/src/components/loginPage/loginForm/LoginForm.tsx
@@ -1,23 +1,15 @@
import SVGIcon from '@components/icons/SVGIcon';
import { UserLoginRequest } from '@myTypes/user/remote';
import { useLogin } from '@hooks/queries/user';
-import useFormInput from '@hooks/_common/useFormInput';
import * as S from './LoginForm.styles';
+import { useForm } from 'react-lightweight-form';
const LoginForm = () => {
- const {
- formState: loginData,
- handleInputChange,
- handleSubmit,
- } = useFormInput({
- identifier: '',
- password: '',
- });
-
+ const { register, handleSubmit } = useForm();
const { login } = useLogin();
- const onSubmit = () => {
- login(loginData);
+ const onSubmit = (formData: UserLoginRequest) => {
+ login(formData);
};
return (
@@ -25,13 +17,12 @@ const LoginForm = () => {
-
+
diff --git a/client/src/components/mainPage/MainPage.tsx b/client/src/components/mainPage/MainPage.tsx
deleted file mode 100644
index 090b88d27..000000000
--- a/client/src/components/mainPage/MainPage.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import {
- DialogBackdrop,
- DialogBox,
- DialogContent,
- DialogTrigger,
-} from '../_common/dialog/dialog';
-import styled from 'styled-components';
-
-const BackDrop = styled.div`
- position: fixed;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
-
- background-color: black;
-`;
-
-const Trigger = styled.div`
- width: 1rem;
- height: 1rem;
- background-color: black;
-`;
-
-const MainPage = () => {
- return (
- // 모달을 사용하고싶은 곳에서 최상위로 꼭 DialogBox를 감싸줘야합니다
-
- {/* 눌렀을 때 모달이 열고 닫히는 trigger버튼이에요. 골룸에서는 전체보기 or 크게보기 버틴이 되겠죠? asChild 속성을 준 후에, 스타일링하고싶은 컴포넌트 꼭 1개만 자식으로 줘야해요 */}
-
- {/* 이렇게 커스텀 된 trigger버튼 1개만!! */}
-
-
- {/* 모달 뜨면 뒤에 생기는 배경입니다! 이것도 asChild 속성을 준 후에 스타일링하고싶은 컴포넌트 꼭 1개만 자식으로 줘야합니다. */}
-
- {/* 이렇게 커스텀 된 backdreop 1개만!! */}
-
-
- {/* 모달의 내용물이 들어가는 부분입니다. 이부분은 asChild 속성을 주면 안되고, children을 줘서 마음대로 모달 내용물을 넣어주면 됩니다~ */}
-
- 모달 내용물~
-
-
- );
-};
-
-export default MainPage;
diff --git a/client/src/components/roadmapCreatePage/category/Category.tsx b/client/src/components/roadmapCreatePage/category/Category.tsx
index 7b8efca2e..9ea30272c 100644
--- a/client/src/components/roadmapCreatePage/category/Category.tsx
+++ b/client/src/components/roadmapCreatePage/category/Category.tsx
@@ -2,49 +2,46 @@ import { CategoriesInfo } from '@/constants/roadmap/category';
import { useSelect } from '@/hooks/_common/useSelect';
import { getInvariantObjectKeys, invariantOf } from '@/utils/_common/invariantType';
import { useEffect } from 'react';
-import { Select, SelectBox } from '../selector/SelectBox';
+import { Select } from 'ck-util-components';
import { S } from './category.styles';
-// 임시 더미데이터
-export type DummyCategoryType = {
- [key: number]: string;
-};
-
type CategoryProps = {
- getSelectedCategoryId: (category: keyof DummyCategoryType | null) => void;
+ getSelectedCategoryId: (category: keyof typeof CategoriesInfo) => void;
};
const Category = ({ getSelectedCategoryId }: CategoryProps) => {
- const { selectOption, selectedOption } = useSelect();
+ const { selectOption, selectedOption } = useSelect();
useEffect(() => {
- getSelectedCategoryId(selectedOption);
+ if (selectedOption !== null) {
+ getSelectedCategoryId(selectedOption);
+ }
}, [selectedOption]);
return (
-
-
-
- 카테고리*
-
-
-
-
- 컨텐츠에 어울리는 카테고리를 선택해주세요.
-
-
+
+
+ 카테고리*
+
+
+ 컨텐츠에 어울리는 카테고리를 선택해주세요.
+
{getInvariantObjectKeys(invariantOf(CategoriesInfo)).map((categoryId) => {
return (
-
+
{CategoriesInfo[categoryId].name}
);
})}
-
+
);
};
diff --git a/client/src/components/roadmapCreatePage/difficulty/Difficulty.tsx b/client/src/components/roadmapCreatePage/difficulty/Difficulty.tsx
index c5b07ce0a..4e61e00c4 100644
--- a/client/src/components/roadmapCreatePage/difficulty/Difficulty.tsx
+++ b/client/src/components/roadmapCreatePage/difficulty/Difficulty.tsx
@@ -1,9 +1,13 @@
/* eslint-disable react/no-unused-prop-types */
import { useSelect } from '@/hooks/_common/useSelect';
import { DifficultiesType, DifficultyKeyType } from '@/myTypes/roadmap/internal';
-import { getInvariantObjectKeys, invariantOf } from '@/utils/_common/invariantType';
+import {
+ getInvariantObjectKeys,
+ getInvariantObjectValues,
+ invariantOf,
+} from '@/utils/_common/invariantType';
import { useEffect } from 'react';
-import { Select, SelectBox } from '../selector/SelectBox';
+import { Select } from 'ck-util-components';
import * as S from './difficulty.styles';
const Difficulties: DifficultiesType = {
@@ -19,54 +23,42 @@ type DifficultyProps = {
};
const Difficulty = ({ getSelectedDifficulty }: DifficultyProps) => {
- const { selectOption, selectedOption } = useSelect();
+ const { selectOption, selectedOption } = useSelect();
useEffect(() => {
if (selectedOption === null) return;
- getSelectedDifficulty(
- getInvariantObjectKeys(invariantOf(Difficulties))[selectedOption]
- );
+ getSelectedDifficulty(selectedOption);
}, [selectedOption]);
return (
-
-
-
- 난이도*
-
-
-
-
- 컨텐츠의 달성 난이도를 선택해주세요
-
-
+
+
+ 난이도*
+
+
+ 컨텐츠의 달성 난이도를 선택해주세요
+
-
-
- {({ selectedId }: { selectedId: number | null }) => {
- return (
-
- {selectedId === null
- ? '선택안함'
- : Difficulties[
- getInvariantObjectKeys(invariantOf(Difficulties))[selectedId]
- ]}
-
- );
- }}
-
+
+ {selectedOption ?? '선택안함'}
{getInvariantObjectKeys(invariantOf(Difficulties)).map((difficulty, idx) => {
return (
-
+
-
+
-
+
{Difficulties[difficulty]}
@@ -74,7 +66,7 @@ const Difficulty = ({ getSelectedDifficulty }: DifficultyProps) => {
})}
-
+
);
};
diff --git a/client/src/components/roadmapCreatePage/difficulty/difficulty.styles.ts b/client/src/components/roadmapCreatePage/difficulty/difficulty.styles.ts
index 7d6684b0b..7089b6992 100644
--- a/client/src/components/roadmapCreatePage/difficulty/difficulty.styles.ts
+++ b/client/src/components/roadmapCreatePage/difficulty/difficulty.styles.ts
@@ -34,6 +34,12 @@ export const Wrapper = styled.ul`
`;
export const TriggerButton = styled.button`
+ cursor: pointer;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
width: 15.4rem;
height: 4rem;
diff --git a/client/src/components/roadmapCreatePage/roadmapCreateForm/RoadmapCreateForm.tsx b/client/src/components/roadmapCreatePage/roadmapCreateForm/RoadmapCreateForm.tsx
index 2c11169b6..ccfc66a3a 100644
--- a/client/src/components/roadmapCreatePage/roadmapCreateForm/RoadmapCreateForm.tsx
+++ b/client/src/components/roadmapCreatePage/roadmapCreateForm/RoadmapCreateForm.tsx
@@ -1,5 +1,4 @@
import { useCollectRoadmapData } from '@/hooks/roadmap/useCollectRoadmapData';
-import React, { createContext, PropsWithChildren, useRef } from 'react';
import Category from '../category/Category';
import Description from '../description/Description';
import Difficulty from '../difficulty/Difficulty';
@@ -11,18 +10,6 @@ import Tag from '../tag/Tag';
import Title from '../title/Title';
import * as S from './roadmapCreateForm.styles';
-// ref공유를 위한 context - 다음 브랜치에서 파일 옮길 예정
-const FormRefContext = createContext<{ ref: React.MutableRefObject | null }>({
- ref: null,
-});
-
-const RefProvider = ({ children }: PropsWithChildren) => {
- const ref = useRef();
-
- return {children};
-};
-//
-
const RoadmapCreateForm = () => {
const {
roadmapValue,
@@ -36,7 +23,7 @@ const RoadmapCreateForm = () => {
} = useCollectRoadmapData();
return (
-
+ <>
로드맵
을 생성해주세요
@@ -75,7 +62,7 @@ const RoadmapCreateForm = () => {
로드맵 생성완료
-
+ >
);
};
diff --git a/client/src/components/roadmapCreatePage/selector/SelectBox.tsx b/client/src/components/roadmapCreatePage/selector/SelectBox.tsx
deleted file mode 100644
index d204ff88d..000000000
--- a/client/src/components/roadmapCreatePage/selector/SelectBox.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-import React, { cloneElement, PropsWithChildren, ReactElement, useEffect } from 'react';
-import { useContextScope } from '@/hooks/_common/useContextScope';
-import { combineStates, getCustomElement } from '@/hooks/_common/compound';
-import {
- DescriptionProps,
- externalStateType,
- IndicatorProps,
- LabelProps,
- OptionGroupProps,
- OptionProps,
- SelectBoxProps,
- SelectContextType,
- TriggerProps,
- ValueProps,
-} from '@/myTypes/_common/select';
-import { useSelect } from '@/hooks/_common/useSelect';
-import { SelectContext } from '@/context/selectContext';
-import { S } from './selectBox.styles';
-
-// select컴포넌트가 context를 공유할 수 있게 하는 provider컴포넌트
-export const SelectBox = (
- props: PropsWithChildren>
-) => {
- const { children, defaultOpen, externalSelectState } = props;
- const {
- selectedOption: selectedId,
- selectOption: innerSelectState,
- isSelecBoxOpen,
- toggleBoxOpen,
- } = useSelect(defaultOpen);
- const selectOption = combineStates(externalSelectState, innerSelectState);
-
- return (
-
- {children}
-
- );
-};
-
-// select컴포넌트의 라벨
-export const Label = (props: PropsWithChildren) => {
- const { asChild = false, children, ...restProps } = props;
-
- if (asChild) {
- return getCustomElement(children as ReactElement, { ...restProps });
- }
- return {children};
-};
-
-// select컴포넌트에 대한 설명
-export const Description = (props: PropsWithChildren) => {
- const { asChild = false, children, ...restProps } = props;
-
- if (asChild) {
- return getCustomElement(children as ReactElement, { ...restProps });
- }
- return {children};
-};
-
-// 클릭하면 selectBox를 보여줄 수 있는 trigger 버튼
-export const Trigger = (props: PropsWithChildren) => {
- const { asChild = false, children, ...restProps } = props;
- const { toggleBoxOpen } = useContextScope(SelectContext);
-
- if (asChild) {
- return getCustomElement(children as ReactElement, {
- ...restProps,
- onClick: (e: React.MouseEvent) => {
- e.preventDefault();
- toggleBoxOpen();
- },
- });
- }
- return (
- ) => {
- e.preventDefault();
- toggleBoxOpen();
- }}
- >
- {children}
-
- );
-};
-
-export const Value = (props: ValueProps) => {
- const { asChild = false, children, ...restProps } = props;
- console.log(restProps);
- const { selectedId } = useContextScope(SelectContext);
- return cloneElement(children({ selectedId }));
-};
-
-// Option들을 담는 컨테이너 컴포넌트
-export const OptionGroup = (props: PropsWithChildren) => {
- const { asChild = false, children, ...restProps } = props;
- const { isSelecBoxOpen } = useContextScope(SelectContext);
-
- if (asChild) {
- return isSelecBoxOpen
- ? getCustomElement(children as ReactElement, { ...restProps })
- : null;
- }
- return isSelecBoxOpen ? {children} : null;
-};
-
-// Option이 선택되었는지 나타내는 indicator
-export const Indicator = (props: PropsWithChildren) => {
- const { asChild = false, children, ...restProps } = props;
- const { selectedId } = useContextScope(SelectContext);
- const isSelected = restProps.id === selectedId;
-
- if (asChild) {
- return getCustomElement(children as ReactElement, { ...restProps, isSelected });
- }
- return {children};
-};
-
-// select의 각 Option
-export const Option = (props: PropsWithChildren) => {
- const { asChild = false, children, ...restProps } = props;
- const { selectOption, selectedId, toggleBoxOpen } =
- useContextScope(SelectContext);
- const isSelected = restProps.id === selectedId;
-
- useEffect(() => {
- if (restProps.defaultSelected) {
- selectOption(restProps.id);
- }
- }, []);
-
- if (asChild) {
- return getCustomElement(children as ReactElement, {
- ...restProps,
- isSelected,
- onClick: (e: React.MouseEvent) => {
- e.preventDefault();
- selectOption(restProps.id);
- if (!restProps.defaultOpen) {
- toggleBoxOpen();
- }
- },
- });
- }
- return (
- ) => {
- e.preventDefault();
- selectOption(restProps.id);
- }}
- >
- {children}
-
- );
-};
-
-export const Select = Object.assign(SelectBox, {
- Label,
- Description,
- Trigger,
- Value,
- OptionGroup,
- Indicator,
- Option,
-});
diff --git a/client/src/components/roadmapCreatePage/selector/selectBox.styles.ts b/client/src/components/roadmapCreatePage/selector/selectBox.styles.ts
deleted file mode 100644
index 6fbc512d3..000000000
--- a/client/src/components/roadmapCreatePage/selector/selectBox.styles.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import styled from 'styled-components';
-
-const DefaultLabel = styled.div`
- ${({ theme }) => theme.fonts.title_large}
-`;
-
-const DefaultDescription = styled.div`
- ${({ theme }) => theme.fonts.description5}
-`;
-
-const DefaultTrigger = styled.div`
- width: 2rem;
- height: 2rem;
-`;
-
-const DefaultIndicator = styled.div<{ isSelected: boolean }>`
- width: 0.2rem;
- height: 0.2rem;
-`;
-
-const DefaultOptionGroup = styled.div`
- width: 2rem;
-`;
-
-const DefaultOption = styled.div<{ isSelected: boolean }>`
- width: 8rem;
- height: 2rem;
-`;
-
-export const S = {
- DefaultLabel,
- DefaultDescription,
- DefaultTrigger,
- DefaultIndicator,
- DefaultOptionGroup,
- DefaultOption,
-};
diff --git a/client/src/components/roadmapListPage/categories/Categories.tsx b/client/src/components/roadmapListPage/categories/Categories.tsx
index 666bae3da..156cff2f0 100644
--- a/client/src/components/roadmapListPage/categories/Categories.tsx
+++ b/client/src/components/roadmapListPage/categories/Categories.tsx
@@ -1,4 +1,3 @@
-import { MouseEvent } from 'react';
import type { CategoryType, SelectedCategoryId } from '@myTypes/roadmap/internal';
import { CategoriesInfo } from '@constants/roadmap/category';
import SVGIcon from '@components/icons/SVGIcon';
@@ -7,16 +6,14 @@ import { useNavigate } from 'react-router-dom';
type CategoriesProps = {
selectedCategoryId: SelectedCategoryId;
- selectCategory: ({ currentTarget }: MouseEvent) => void;
};
-const Categories = ({ selectedCategoryId, selectCategory }: CategoriesProps) => {
+const Categories = ({ selectedCategoryId }: CategoriesProps) => {
const categories = Object.values(CategoriesInfo);
const upCategories = categories.slice(0, 5);
const downCategories = categories.slice(5);
const navigate = useNavigate();
- console.log(selectCategory);
const handleClickCategory = (categoryId: CategoryType['id']) => {
const queryParams = new URLSearchParams();
diff --git a/client/src/components/roadmapListPage/roadmapList/RoadmapList.tsx b/client/src/components/roadmapListPage/roadmapList/RoadmapList.tsx
index 25c677bdb..ff9b7ca46 100644
--- a/client/src/components/roadmapListPage/roadmapList/RoadmapList.tsx
+++ b/client/src/components/roadmapListPage/roadmapList/RoadmapList.tsx
@@ -1,17 +1,12 @@
import { useRoadmapList } from '@hooks/queries/roadmap';
import RoadmapItem from '@components/_common/roadmapItem/RoadmapItem';
import * as S from './RoadmapList.styles';
-import { SelectedCategoryId } from '@myTypes/roadmap/internal';
import { useLocation, useNavigate } from 'react-router-dom';
import { useInfiniteScroll } from '@hooks/_common/useInfiniteScroll';
import WavyLoading from '@/components/_common/wavyLoading/WavyLoading';
import NoResult from '@components/roadmapListPage/roadmapSearch/NoResult';
-type RoadmapListProps = {
- selectedCategoryId: SelectedCategoryId;
-};
-
-const RoadmapList = ({ selectedCategoryId }: RoadmapListProps) => {
+const RoadmapList = () => {
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const categoryId = queryParams.get('category');
@@ -30,7 +25,7 @@ const RoadmapList = ({ selectedCategoryId }: RoadmapListProps) => {
const moveRoadmapCreatePage = () => {
navigate('/roadmap-create');
};
- console.log(selectedCategoryId);
+
return (
{!roadmapListResponse.responses.length && }
diff --git a/client/src/components/roadmapListPage/roadmapListView/RoadmapListView.tsx b/client/src/components/roadmapListPage/roadmapListView/RoadmapListView.tsx
index 874e32db5..c8d3b2672 100644
--- a/client/src/components/roadmapListPage/roadmapListView/RoadmapListView.tsx
+++ b/client/src/components/roadmapListPage/roadmapListView/RoadmapListView.tsx
@@ -1,18 +1,15 @@
-import { Suspense } from 'react';
import Categories from '../categories/Categories';
-
import * as S from './RoadmapListView.styles';
import { useSelectCategory } from '@/hooks/roadmap/useSelectCategory';
import RoadmapList from '../roadmapList/RoadmapList';
-import Spinner from '@components/_common/spinner/Spinner';
import RoadmapSearch from '../roadmapSearch/RoadmapSearch';
-// import { Select } from '@/components/roadmapCreatePage/selector/SelectBox';
import { Link, Outlet } from 'react-router-dom';
import useValidParams from '@/hooks/_common/useValidParams';
import SVGIcon from '@/components/icons/SVGIcon';
+import AsyncBoundary from '@/components/_common/errorBoundary/AsyncBoundary';
const RoadmapListView = () => {
- const [selectedCategoryId, selectCategory] = useSelectCategory();
+ const [selectedCategoryId] = useSelectCategory();
const { search } = useValidParams();
return (
@@ -23,21 +20,12 @@ const RoadmapListView = () => {
-
+
- }>
+
- {!search && (
-
- )}
-
+ {!search && }
+
);
};
diff --git a/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearch.tsx b/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearch.tsx
index 1bded6fbb..b0cceeb2b 100644
--- a/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearch.tsx
+++ b/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearch.tsx
@@ -1,15 +1,10 @@
import { SearchIcon } from '@/components/icons/svgIcons';
-import { Select } from '@/components/roadmapCreatePage/selector/SelectBox';
+import { getInvariantObjectKeys, invariantOf } from '@/utils/_common/invariantType';
+import { Select } from 'ck-util-components';
import { FormEvent, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import * as S from './roadmapSearch.styles';
-const searchCategoryKeyword = {
- 1: 'tagName',
- 2: 'roadmapTitle',
- 3: 'creatorName',
-} as const;
-
const searchCategorySelection = {
tagName: '태그',
roadmapTitle: '로드맵 제목',
@@ -22,12 +17,15 @@ const RoadmapSearch = () => {
const [searchCategory, setSearchCategory] = useState<
'tagName' | 'roadmapTitle' | 'creatorName'
>('roadmapTitle');
+ const [categoryOpen, setCategoryOpen] = useState(false);
+
+ const selectSearchCategory = (option: keyof typeof searchCategorySelection) => {
+ setSearchCategory(option);
+ };
- const selectSearchCategory = (id: number) => {
- // eslint-disable-next-line no-prototype-builtins
- if (searchCategory.hasOwnProperty(id)) {
- setSearchCategory(searchCategoryKeyword[id as keyof typeof searchCategoryKeyword]);
- }
+ const toggleSearchCategory = () => {
+ // eslint-disable-next-line no-unused-expressions
+ categoryOpen ? setCategoryOpen(false) : setCategoryOpen(true);
};
const searchRoadmap = (e: FormEvent) => {
@@ -42,7 +40,12 @@ const RoadmapSearch = () => {