diff --git a/client/src/App.tsx b/client/src/App.tsx
index 233c214c6..faca6a168 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,5 +1,5 @@
-import { lazy, PropsWithChildren, Suspense, useEffect } from 'react';
-import { BrowserRouter, Route, Routes, useNavigate } from 'react-router-dom';
+import { lazy, Suspense } from 'react';
+import { BrowserRouter, Route, Routes } from 'react-router-dom';
import theme from '@styles/theme';
import GlobalStyle from '@styles/GlobalStyle';
import { ThemeProvider } from 'styled-components';
@@ -13,16 +13,14 @@ import RoadmapDetailPage from './pages/roadmapDetailPage/RoadmapDetailPage';
import RoadmapCreatePage from './pages/roadmapCreatePage/RoadmapCreatePage';
import ToastProvider from '@components/_common/toastProvider/ToastProvider';
import MyPage from '@pages/myPage/MyPage';
-import UserInfoProvider, {
- useUserInfoContext,
-} from './components/_providers/UserInfoProvider';
+import UserInfoProvider from './components/_providers/UserInfoProvider';
import RoadmapSearchResult from './components/roadmapListPage/roadmapSearch/RoadmapSearchResult';
import MainPage from '@pages/mainPage/MainPage';
-import useToast from '@hooks/_common/useToast';
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';
const GoalRoomDashboardPage = lazy(
() => import('@pages/goalRoomDashboardPage/GoalRoomDashboardPage')
@@ -32,22 +30,6 @@ const GoalRoomCreatePage = lazy(
() => import('@pages/goalRoomCreatePage/GoalRoomCreatePage')
);
-const PrivateRouter = (props: PropsWithChildren) => {
- const { children } = props;
- const { userInfo } = useUserInfoContext();
- const { triggerToast } = useToast();
- const navigate = useNavigate();
-
- useEffect(() => {
- if (userInfo.id === null) {
- navigate('/login');
- triggerToast({ message: '로그인이 필요한 서비스입니다.' });
- }
- }, [userInfo.id, navigate]);
-
- return <>{children}>;
-};
-
const App = () => {
return (
diff --git a/client/src/components/_common/privateRouter/PrivateRouter.tsx b/client/src/components/_common/privateRouter/PrivateRouter.tsx
new file mode 100644
index 000000000..c02e0323d
--- /dev/null
+++ b/client/src/components/_common/privateRouter/PrivateRouter.tsx
@@ -0,0 +1,24 @@
+import { PropsWithChildren, useEffect } from 'react';
+import { useUserInfoContext } from '@components/_providers/UserInfoProvider';
+import useToast from '@hooks/_common/useToast';
+import { useNavigate } from 'react-router-dom';
+import { getCookie } from '@utils/_common/cookies';
+
+const PrivateRouter = (props: PropsWithChildren) => {
+ const { children } = props;
+ const { userInfo } = useUserInfoContext();
+ const { triggerToast } = useToast();
+ const navigate = useNavigate();
+ const accessToken = getCookie('access_token');
+
+ useEffect(() => {
+ if (userInfo.id === null && !accessToken) {
+ navigate('/login');
+ triggerToast({ message: '로그인이 필요한 서비스입니다.' });
+ }
+ }, [userInfo.id, navigate]);
+
+ return <>{children}>;
+};
+
+export default PrivateRouter;
diff --git a/client/src/components/_common/slider/Slider.styles.ts b/client/src/components/_common/slider/Slider.styles.ts
new file mode 100644
index 000000000..25868a8c6
--- /dev/null
+++ b/client/src/components/_common/slider/Slider.styles.ts
@@ -0,0 +1,58 @@
+import media from '@styles/media';
+import styled, { css } from 'styled-components';
+
+export const Slider = styled.div`
+ position: relative;
+
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+
+ width: 100%;
+`;
+
+export const Button = styled.button<{ isHovered: boolean }>`
+ position: absolute;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ width: 7rem;
+ height: 12rem;
+
+ background-color: rgba(1, 1, 1, 0.2);
+ border-radius: 8px;
+ box-shadow: ${({ theme }) => theme.shadows.box};
+
+ ${({ isHovered }) =>
+ media.desktop(css`
+ opacity: ${isHovered ? 1 : 0};
+ transition: opacity 0.2s ease;
+ `)}
+`;
+
+export const PrevButton = styled(Button)<{ isFirstContentIndex: boolean }>`
+ left: 2rem;
+ display: ${({ isFirstContentIndex }) => isFirstContentIndex && 'none'};
+`;
+
+export const NextButton = styled(Button)<{ isLastContentIndex: boolean }>`
+ right: 2rem;
+ display: ${({ isLastContentIndex }) => isLastContentIndex && 'none'};
+`;
+
+export const Contents = styled.article<{ curIndex: number; length: number }>`
+ transform: ${({ curIndex }) => `translateX(${-curIndex * 100}%)`};
+ display: flex;
+ width: ${({ length }) => `${length * 100}%`};
+ transition: transform 0.3s ease;
+`;
+
+export const Content = styled.div`
+ flex-shrink: 0;
+ width: 100%;
+ & > * {
+ min-height: 12rem;
+ }
+`;
diff --git a/client/src/components/_common/slider/Slider.tsx b/client/src/components/_common/slider/Slider.tsx
new file mode 100644
index 000000000..5db87845b
--- /dev/null
+++ b/client/src/components/_common/slider/Slider.tsx
@@ -0,0 +1,43 @@
+import { PropsWithChildren } from 'react';
+import * as S from './Slider.styles';
+import useHover from '@hooks/_common/useHover';
+import SVGIcon from '@components/icons/SVGIcon';
+import useSlider from '@hooks/_common/useSlider';
+
+const Slider = ({ children }: PropsWithChildren) => {
+ const {
+ curIndex,
+ slideToPrevContent,
+ slideToNextContent,
+ isFirstContentIndex,
+ isLastContentIndex,
+ childrenArray,
+ } = useSlider(children);
+ const { isHovered, handleMouseEnter, handleMouseLeave } = useHover();
+
+ return (
+
+
+ {childrenArray.map((child, index) => (
+ {child}
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Slider;
diff --git a/client/src/components/icons/svgIcons.tsx b/client/src/components/icons/svgIcons.tsx
index e9dbf5940..1c766aa6a 100644
--- a/client/src/components/icons/svgIcons.tsx
+++ b/client/src/components/icons/svgIcons.tsx
@@ -2454,3 +2454,52 @@ export const ImageUploadIcon = ({ width, ...props }: SVGProps) =>
/>
);
+
+export const LeftIcon = ({ width, ...props }: SVGProps) => (
+
+);
+
+export const RightIcon = ({ width, ...props }: SVGProps) => (
+
+);
+
+export const NoImageIcon = ({ width, ...props }: SVGProps) => (
+
+);
diff --git a/client/src/components/roadmapCreatePage/difficulty/Difficulty.tsx b/client/src/components/roadmapCreatePage/difficulty/Difficulty.tsx
index 8e6dcdb88..c5b07ce0a 100644
--- a/client/src/components/roadmapCreatePage/difficulty/Difficulty.tsx
+++ b/client/src/components/roadmapCreatePage/difficulty/Difficulty.tsx
@@ -1,31 +1,32 @@
/* 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 { useEffect } from 'react';
import { Select, SelectBox } from '../selector/SelectBox';
import * as S from './difficulty.styles';
-// 임시 더미데이터
-export type DummyDifficultyType = {
- [key: number]: string;
+const Difficulties: DifficultiesType = {
+ VERY_EASY: '매우쉬움',
+ EASY: '쉬움',
+ NORMAL: '보통',
+ DIFFICULT: '어려움',
+ VERY_DIFFICULT: '매우어려움',
};
-const DummyDifficulty: DummyDifficultyType = {
- 1: '매우쉬움',
- 2: '쉬움',
- 3: '보통',
- 4: '어려움',
- 5: '매우어려움',
+type DifficultyProps = {
+ getSelectedDifficulty: (difficulty: DifficultyKeyType | null) => void;
};
-type DifficultyType = {
- getSelectedDifficulty: (difficulty: keyof DummyDifficultyType | null) => void;
-};
-
-const Difficulty = ({ getSelectedDifficulty }: DifficultyType) => {
+const Difficulty = ({ getSelectedDifficulty }: DifficultyProps) => {
const { selectOption, selectedOption } = useSelect();
useEffect(() => {
- getSelectedDifficulty(selectedOption);
+ if (selectedOption === null) return;
+
+ getSelectedDifficulty(
+ getInvariantObjectKeys(invariantOf(Difficulties))[selectedOption]
+ );
}, [selectedOption]);
return (
@@ -46,7 +47,11 @@ const Difficulty = ({ getSelectedDifficulty }: DifficultyType) => {
{({ selectedId }: { selectedId: number | null }) => {
return (
- {selectedId === null ? '선택안함' : DummyDifficulty[selectedId]}
+ {selectedId === null
+ ? '선택안함'
+ : Difficulties[
+ getInvariantObjectKeys(invariantOf(Difficulties))[selectedId]
+ ]}
);
}}
@@ -55,14 +60,14 @@ const Difficulty = ({ getSelectedDifficulty }: DifficultyType) => {
- {Object.keys(DummyDifficulty).map((difficultyId) => {
+ {getInvariantObjectKeys(invariantOf(Difficulties)).map((difficulty, idx) => {
return (
-
+
-
+
- {DummyDifficulty[Number(difficultyId)]}
+ {Difficulties[difficulty]}
);
diff --git a/client/src/components/roadmapCreatePage/roadmapCreateForm/RoadmapCreateForm.tsx b/client/src/components/roadmapCreatePage/roadmapCreateForm/RoadmapCreateForm.tsx
index a84968ee7..2c11169b6 100644
--- a/client/src/components/roadmapCreatePage/roadmapCreateForm/RoadmapCreateForm.tsx
+++ b/client/src/components/roadmapCreatePage/roadmapCreateForm/RoadmapCreateForm.tsx
@@ -50,6 +50,14 @@ const RoadmapCreateForm = () => {
<>
+ {roadmapValue.roadmapNodes.length === 0 && (
+
+ )}
{roadmapValue.roadmapNodes.map((_, index) => {
return (
void;
};
const Tag = ({ getTags }: TagProps) => {
- const { tags, ref, addTagByButton, addTagByEnter, checkIsTagCountMax, deleteTag } =
- useCreateTag();
+ const {
+ tags,
+ ref,
+ addTagByButton,
+ addTagByEnter,
+ checkIsTagCountMax,
+ checkIsAddCountMax,
+ deleteTag,
+ } = useCreateTag();
useEffect(() => {
getTags(tags);
@@ -29,8 +36,10 @@ const Tag = ({ getTags }: TagProps) => {
>
))}
-
- {checkIsTagCountMax() && +}
+ {checkIsTagCountMax() && (
+
+ )}
+ {checkIsAddCountMax() && +}
);
diff --git a/client/src/components/roadmapDetailPage/extraInfo/ExtraInfo.styles.ts b/client/src/components/roadmapDetailPage/extraInfo/ExtraInfo.styles.ts
new file mode 100644
index 000000000..98aa0449c
--- /dev/null
+++ b/client/src/components/roadmapDetailPage/extraInfo/ExtraInfo.styles.ts
@@ -0,0 +1,48 @@
+import styled from 'styled-components';
+import media from '@styles/media';
+
+export const ExtraInfo = styled.div`
+ ${({ theme }) => theme.fonts.description5};
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ justify-content: space-between;
+
+ width: 30%;
+ height: 55rem;
+
+ ${media.mobile`
+ display: none;
+ `}
+`;
+
+export const RoadmapMetadata = styled.div`
+ display: flex;
+ flex-direction: column;
+
+ & > div:not(:last-child) {
+ margin-bottom: 3rem;
+ }
+`;
+
+export const Category = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+`;
+
+export const Difficulty = styled.div`
+ text-align: end;
+`;
+
+export const RecommendedRoadmapPeriod = styled.div`
+ text-align: end;
+`;
+
+export const Tags = styled.div`
+ color: ${({ theme }) => theme.colors.main_dark};
+
+ & > div {
+ text-align: end;
+ }
+`;
diff --git a/client/src/components/roadmapDetailPage/extraInfo/ExtraInfo.tsx b/client/src/components/roadmapDetailPage/extraInfo/ExtraInfo.tsx
new file mode 100644
index 000000000..4959bca11
--- /dev/null
+++ b/client/src/components/roadmapDetailPage/extraInfo/ExtraInfo.tsx
@@ -0,0 +1,33 @@
+import { RoadmapDetailType } from '@myTypes/roadmap/internal';
+import * as S from './ExtraInfo.styles';
+import SVGIcon from '@components/icons/SVGIcon';
+import { CategoriesInfo } from '@constants/roadmap/category';
+
+type ExtraInfoProps = {
+ roadmapInfo: RoadmapDetailType;
+};
+
+const ExtraInfo = ({ roadmapInfo }: ExtraInfoProps) => {
+ return (
+
+ Created by {roadmapInfo.creator.name}
+
+
+ 카테고리: {roadmapInfo.category.name}
+
+
+ 난이도: {roadmapInfo.difficulty}
+
+ 예상 소요시간: {roadmapInfo.recommendedRoadmapPeriod}일
+
+
+
+ {roadmapInfo.tags.map((tag) => (
+ #{tag.name}
+ ))}
+
+
+ );
+};
+
+export default ExtraInfo;
diff --git a/client/src/components/roadmapDetailPage/introduction/Introduction.styles.ts b/client/src/components/roadmapDetailPage/introduction/Introduction.styles.ts
new file mode 100644
index 000000000..bd5f1e6bf
--- /dev/null
+++ b/client/src/components/roadmapDetailPage/introduction/Introduction.styles.ts
@@ -0,0 +1,47 @@
+import media from '@styles/media';
+import styled from 'styled-components';
+
+export const IntroductionWrapper = styled.div`
+ width: 70%;
+
+ ${media.mobile`
+ width:100%;
+ `}
+`;
+
+export const Introduction = styled.div<{ isExpanded: boolean }>`
+ ${({ theme }) => theme.fonts.description5};
+ overflow: hidden;
+ max-height: ${({ isExpanded }) => (isExpanded ? 'auto' : '55rem')};
+
+ & > p:not(:last-child) {
+ margin-bottom: 2rem;
+ }
+
+ & div {
+ ${({ theme }) => theme.fonts.h1};
+ margin-bottom: 0.5rem;
+ color: ${({ theme }) => theme.colors.main_dark};
+ }
+`;
+
+export const LineShadow = styled.div`
+ position: relative;
+ width: 100%;
+ height: 0.2rem;
+ box-shadow: 0 -4px 6px rgba(0, 0, 0, 1);
+`;
+
+export const ReadMoreButton = styled.button`
+ position: relative;
+ top: calc(-2rem - 4px);
+ left: 50%;
+ transform: translateX(-5rem);
+
+ width: 10rem;
+ height: 4rem;
+
+ background-color: ${({ theme }) => theme.colors.white};
+ border-radius: 8px;
+ box-shadow: ${({ theme }) => theme.shadows.main};
+`;
diff --git a/client/src/components/roadmapDetailPage/introduction/Introduction.tsx b/client/src/components/roadmapDetailPage/introduction/Introduction.tsx
new file mode 100644
index 000000000..3069efacf
--- /dev/null
+++ b/client/src/components/roadmapDetailPage/introduction/Introduction.tsx
@@ -0,0 +1,51 @@
+import { RoadmapDetailType } from '@myTypes/roadmap/internal';
+import * as S from './Introduction.styles';
+import { useEffect, useRef, useState } from 'react';
+
+type IntroductionProps = {
+ roadmapInfo: RoadmapDetailType;
+};
+
+const Introduction = ({ roadmapInfo }: IntroductionProps) => {
+ const [isExpanded, setIsExpanded] = useState(false);
+ const [showMoreButton, setShowMoreButton] = useState(false);
+ const introRef = useRef(null);
+
+ useEffect(() => {
+ if (!introRef.current) return;
+
+ const element = introRef.current;
+ if (element.scrollHeight > element.clientHeight) {
+ setShowMoreButton(true);
+ }
+ }, []);
+
+ const toggleExpand = () => {
+ setIsExpanded((prev) => !prev);
+ };
+
+ return (
+
+
+
+
설명
+ {roadmapInfo.introduction}
+
+
+
본문
+ {roadmapInfo.content.content === ''
+ ? '로드맵에 대한 설명이 없어요🥲'
+ : roadmapInfo.content.content}
+
+
+ {showMoreButton && !isExpanded && (
+ <>
+
+ 더 보기
+ >
+ )}
+
+ );
+};
+
+export default Introduction;
diff --git a/client/src/components/roadmapDetailPage/nodeContent/NodeContent.styles.ts b/client/src/components/roadmapDetailPage/nodeContent/NodeContent.styles.ts
new file mode 100644
index 000000000..695e6a26b
--- /dev/null
+++ b/client/src/components/roadmapDetailPage/nodeContent/NodeContent.styles.ts
@@ -0,0 +1,90 @@
+import styled from 'styled-components';
+import media from '@styles/media';
+
+export const SliderContent = styled.div`
+ display: flex;
+ aspect-ratio: 5 / 3.5;
+ background-color: ${({ theme }) => theme.colors.gray100};
+ border-radius: 8px;
+
+ ${media.mobile`
+ aspect-ratio: 0;
+ `}
+`;
+
+export const LeftContent = styled.div`
+ width: 45%;
+
+ ${media.mobile`
+ display: none;
+ `}
+`;
+
+export const NodeImg = styled.img`
+ width: 100%;
+ height: 100%;
+ padding: 1.5rem;
+ object-fit: cover;
+`;
+
+export const NoImg = styled.div`
+ ${({ theme }) => theme.fonts.title_large}
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ width: 100%;
+ height: 100%;
+`;
+
+export const Separator = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 0.2rem;
+ height: 100%;
+
+ & > div {
+ height: 50%;
+ }
+
+ & > div:last-child {
+ background-color: black;
+ }
+`;
+
+export const RightContent = styled.div`
+ ${({ theme }) => theme.fonts.h1}
+ overflow: scroll;
+ width: 55%;
+ padding: 1.5rem;
+ padding-top: 3rem;
+
+ ${media.mobile`
+ width: 100%;
+ height: 60rem;
+ padding-top: 1.5rem;
+ `}
+`;
+
+export const ContentTitle = styled.div`
+ ${({ theme }) => theme.fonts.title_large}
+ display: flex;
+ align-items: center;
+ margin-bottom: 1rem;
+`;
+
+export const Step = styled.div`
+ ${({ theme }) => theme.fonts.h2}
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+ justify-content: center;
+
+ width: 3rem;
+ height: 3rem;
+ margin-right: 0.5rem;
+
+ border: 0.3rem solid black;
+ border-radius: 50%;
+`;
diff --git a/client/src/components/roadmapDetailPage/nodeContent/NodeContent.tsx b/client/src/components/roadmapDetailPage/nodeContent/NodeContent.tsx
new file mode 100644
index 000000000..f2ffb7e60
--- /dev/null
+++ b/client/src/components/roadmapDetailPage/nodeContent/NodeContent.tsx
@@ -0,0 +1,38 @@
+import type { NodeType } from '@myTypes/roadmap/internal';
+import * as S from './NodeContent.styles';
+import SVGIcon from '@components/icons/SVGIcon';
+
+type NodeContentProps = {
+ node: NodeType;
+ index: number;
+};
+
+const NodeContent = ({ node, index }: NodeContentProps) => {
+ return (
+
+
+ {node.imageUrls[0] ? (
+
+ ) : (
+
+
+ No Image
+
+ )}
+
+
+
+
+
+
+
+ {index + 1}
+ {node.title}
+
+ {node.description}
+
+
+ );
+};
+
+export default NodeContent;
diff --git a/client/src/components/roadmapDetailPage/roadmapDetail/RoadmapDetail.styles.ts b/client/src/components/roadmapDetailPage/roadmapDetail/RoadmapDetail.styles.ts
index 4cafaf5cf..93c0b17ce 100644
--- a/client/src/components/roadmapDetailPage/roadmapDetail/RoadmapDetail.styles.ts
+++ b/client/src/components/roadmapDetailPage/roadmapDetail/RoadmapDetail.styles.ts
@@ -1,43 +1,52 @@
-import media from '@styles/media';
import styled from 'styled-components';
export const RoadmapDetail = styled.div`
- position: relative;
- margin: 4rem 0;
- padding: 0 2rem;
- white-space: pre-line;
-
- ${media.mobile`
- flex-direction: column;
- align-items: center;
- `}
+ padding: 2rem 0 4rem 0;
`;
-export const RoadmapBody = styled.p`
- ${({ theme }) => theme.fonts.button1}
- width: 50%;
- padding: 4rem 4rem;
- height: 35rem;
+export const RoadmapInfo = styled.div``;
- overflow: scroll;
+export const Title = styled.div`
+ ${({ theme }) => theme.fonts.title_large};
+ margin-bottom: 2rem;
+ color: ${({ theme }) => theme.colors.main_dark};
+`;
- border-radius: 18px;
- box-shadow: ${({ theme }) => theme.shadows.box};
+export const Description = styled.div`
+ display: flex;
+`;
- color: ${({ theme }) => theme.colors.gray300};
+export const ButtonsWrapper = styled.div`
+ position: relative;
+ width: 100%;
+`;
- ${media.mobile`
- padding: 4rem 4rem;
- `}
+export const Buttons = styled.div`
+ bottom: 3rem;
- & > strong {
- ${({ theme }) => theme.fonts.h1};
- margin-bottom: 4rem;
- color: ${({ theme }) => theme.colors.main_dark};
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+
+ margin: 2rem 0;
+
+ background-color: ${({ theme }) => theme.colors.main_dark};
+ border-radius: 8px;
+
+ & > div {
+ width: 0.2rem;
+ height: 5.5rem;
+ background-color: ${({ theme }) => theme.colors.white};
}
`;
-export const PageOnTop = styled.div`
- display: flex;
- justify-content: space-around;
+export const Button = styled.button`
+ ${({ theme }) => theme.fonts.nav_text}
+ width: 50%;
+ height: 5.5rem;
+
+ color: ${({ theme }) => theme.colors.white};
+
+ background-color: ${({ theme }) => theme.colors.main_dark};
+ border-radius: 8px;
`;
diff --git a/client/src/components/roadmapDetailPage/roadmapDetail/RoadmapDetail.tsx b/client/src/components/roadmapDetailPage/roadmapDetail/RoadmapDetail.tsx
index 94da78baa..912425ee5 100644
--- a/client/src/components/roadmapDetailPage/roadmapDetail/RoadmapDetail.tsx
+++ b/client/src/components/roadmapDetailPage/roadmapDetail/RoadmapDetail.tsx
@@ -1,36 +1,41 @@
-import RoadmapItem from '../../_common/roadmapItem/RoadmapItem';
-import Button from '../../_common/button/Button';
import * as S from './RoadmapDetail.styles';
-import useValidParams from '@/hooks/_common/useValidParams';
+import useValidParams from '@hooks/_common/useValidParams';
import { useNavigate } from 'react-router-dom';
-import { useRoadmapDetail } from '@/hooks/queries/roadmap';
-import RoadmapNodeList from '../roadmapNodeList/RoadmapNodeList';
+import { useRoadmapDetail } from '@hooks/queries/roadmap';
+
+import Slider from '@components/_common/slider/Slider';
+import NodeContent from '../nodeContent/NodeContent';
+import ExtraInfo from '../extraInfo/ExtraInfo';
+import Introduction from '../introduction/Introduction';
const RoadmapDetail = () => {
const { id: roadmapId } = useValidParams<{ id: string }>();
const navigate = useNavigate();
const { roadmapInfo } = useRoadmapDetail(Number(roadmapId));
- const moveToGoalRoomCreatePage = () => {
- navigate(`/roadmap/${roadmapId}/goalroom-create`);
- };
-
return (
-
-
-
- 로드맵 설명
- {roadmapInfo.content.content === ''
- ? '로드맵에 대한 설명이 없어요🥲'
- : roadmapInfo.content.content}
-
-
-
-
+
+ {roadmapInfo.roadmapTitle}
+
+
+
+
+
+
+ navigate(`/roadmap/${roadmapId}/goalroom-create`)}>
+ 모임 생성하기
+
+
+ navigate(`/roadmap/${roadmapId}/goalroom-list`)}>
+ 진행중인 모임보기
+
+
+
+ {roadmapInfo.content.nodes.map((node, index) => (
+
+ ))}
+
);
};
diff --git a/client/src/components/roadmapListPage/roadmapList/RoadmapList.tsx b/client/src/components/roadmapListPage/roadmapList/RoadmapList.tsx
index 166299b20..221a65953 100644
--- a/client/src/components/roadmapListPage/roadmapList/RoadmapList.tsx
+++ b/client/src/components/roadmapListPage/roadmapList/RoadmapList.tsx
@@ -20,7 +20,7 @@ const RoadmapList = ({ selectedCategoryId }: RoadmapListProps) => {
});
const loadMoreRef = useInfiniteScroll({
- hasNextPage: roadmapListResponse?.hasNext,
+ hasNextPage: roadmapListResponse.hasNext,
fetchNextPage,
});
@@ -35,7 +35,7 @@ const RoadmapList = ({ selectedCategoryId }: RoadmapListProps) => {
{roadmapListResponse.responses.map((item) => (
))}
- {roadmapListResponse?.hasNext && }
+ {roadmapListResponse.hasNext && }
+
);
diff --git a/client/src/components/roadmapListPage/roadmapSearch/NoResult.tsx b/client/src/components/roadmapListPage/roadmapSearch/NoResult.tsx
new file mode 100644
index 000000000..45bcb5ef5
--- /dev/null
+++ b/client/src/components/roadmapListPage/roadmapSearch/NoResult.tsx
@@ -0,0 +1,7 @@
+import * as S from './roadmapSearch.styles';
+
+const NoResult = () => {
+ return 검색결과가 존재하지 않습니다. 😭;
+};
+
+export default NoResult;
diff --git a/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearch.tsx b/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearch.tsx
index 0a97e2943..635c02c44 100644
--- a/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearch.tsx
+++ b/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearch.tsx
@@ -1,6 +1,6 @@
import { SearchIcon } from '@/components/icons/svgIcons';
import { Select } from '@/components/roadmapCreatePage/selector/SelectBox';
-import { useRef, useState } from 'react';
+import { FormEvent, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import * as S from './roadmapSearch.styles';
@@ -24,11 +24,17 @@ const RoadmapSearch = () => {
}
};
- const searchRoadmap = () => {
+ const searchRoadmap = (e: FormEvent) => {
+ e.preventDefault();
+ if (searchWordRef.current?.value === '') return;
+
navigate(`/roadmap-list/${searchCategory}/${searchWordRef.current?.value}`);
};
const resetSearchResult = () => {
+ if (searchWordRef.current === null) return;
+
+ searchWordRef.current.value = '';
navigate('/roadmap-list');
};
@@ -52,7 +58,7 @@ const RoadmapSearch = () => {
(으)로 검색하기
-
+ ) => searchRoadmap(e)}>
{
ref={searchWordRef}
/>
-
+
diff --git a/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearchResult.tsx b/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearchResult.tsx
index 4382412b1..2f81bdc0a 100644
--- a/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearchResult.tsx
+++ b/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearchResult.tsx
@@ -4,34 +4,37 @@ import { useSearchRoadmapList } from '@/hooks/queries/roadmap';
import { useInfiniteScroll } from '@/hooks/_common/useInfiniteScroll';
import useValidParams from '@/hooks/_common/useValidParams';
import { useNavigate } from 'react-router-dom';
+import NoResult from './NoResult';
import * as S from './roadmapSearch.styles';
const RoadmapSearchResult = () => {
const { category, search } = useValidParams();
-
+ const navigate = useNavigate();
const { searchRoadmapListResponse, fetchNextPage } = useSearchRoadmapList({
category,
search,
});
+ const { hasNext, responses: roadmapList } = searchRoadmapListResponse;
const loadMoreRef = useInfiniteScroll({
- hasNextPage: searchRoadmapListResponse?.hasNext,
+ hasNextPage: hasNext,
fetchNextPage,
});
- const navigate = useNavigate();
-
const moveRoadmapCreatePage = () => {
navigate('/roadmap-create');
};
return (
-
- {searchRoadmapListResponse.responses.map((item) => (
-
- ))}
- {searchRoadmapListResponse?.hasNext && }
- +
-
+ <>
+ {roadmapList.length === 0 && }
+
+ {roadmapList.map((item) => (
+
+ ))}
+ {hasNext && }
+ +
+
+ >
);
};
diff --git a/client/src/components/roadmapListPage/roadmapSearch/roadmapSearch.styles.ts b/client/src/components/roadmapListPage/roadmapSearch/roadmapSearch.styles.ts
index b61f1a4b4..142b511af 100644
--- a/client/src/components/roadmapListPage/roadmapSearch/roadmapSearch.styles.ts
+++ b/client/src/components/roadmapListPage/roadmapSearch/roadmapSearch.styles.ts
@@ -92,7 +92,7 @@ export const CreateRoadmapButton = styled.button`
border-radius: 50%;
`;
-export const InputFlex = styled.div`
+export const InputFlex = styled.form`
display: flex;
align-items: flex-end;
`;
@@ -101,3 +101,17 @@ export const ResetSearchButton = styled.button`
${({ theme }) => theme.fonts.description3}
color: ${({ theme }) => theme.colors.main_dark};
`;
+
+export const NoResultWrapper = styled.h1`
+ ${({ theme }) => theme.fonts.title_large};
+ width: 100%;
+ height: 10rem;
+
+ text-align: center;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ color: ${({ theme }) => theme.colors.main_dark};
+`;
diff --git a/client/src/components/signUpPage/signUpForm/SignUpForm.styles.ts b/client/src/components/signUpPage/signUpForm/SignUpForm.styles.ts
index ff02e9d37..9dc8d429d 100644
--- a/client/src/components/signUpPage/signUpForm/SignUpForm.styles.ts
+++ b/client/src/components/signUpPage/signUpForm/SignUpForm.styles.ts
@@ -39,12 +39,16 @@ export const BoldText = styled.span`
`;
export const SubmitButton = styled.button`
+ ${({ theme }) => theme.fonts.button1};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
width: 100%;
height: 3rem;
margin-top: 1rem;
- padding: 1rem;
+ padding: 2rem;
- ${({ theme }) => theme.fonts.button1};
color: ${({ theme }) => theme.colors.white};
background-color: ${({ theme }) => theme.colors.main_dark};
diff --git a/client/src/constants/roadmap/tag.ts b/client/src/constants/roadmap/tag.ts
index 8c4b6e9f2..6662ea4e7 100644
--- a/client/src/constants/roadmap/tag.ts
+++ b/client/src/constants/roadmap/tag.ts
@@ -1,3 +1,3 @@
-export const TAG_LIMIT = 4;
+export const TAG_LIMIT = 5;
export const TAG_ITEM_MAX_LENGTH = 10;
diff --git a/client/src/hooks/_common/useSlider.ts b/client/src/hooks/_common/useSlider.ts
new file mode 100644
index 000000000..1204c7552
--- /dev/null
+++ b/client/src/hooks/_common/useSlider.ts
@@ -0,0 +1,29 @@
+import React, { useState, ReactNode } from 'react';
+
+const useSlider = (children: ReactNode) => {
+ const [curIndex, setCurIndex] = useState(0);
+ const childrenArray = React.Children.toArray(children) as React.ReactElement[];
+ const contentLength = childrenArray.length;
+
+ const isFirstContentIndex = curIndex === 0;
+ const isLastContentIndex = curIndex === contentLength - 1;
+
+ const slideToPrevContent = () => {
+ setCurIndex((prev) => (isFirstContentIndex ? prev : prev - 1));
+ };
+
+ const slideToNextContent = () => {
+ setCurIndex((prev) => (isLastContentIndex ? prev : prev + 1));
+ };
+
+ return {
+ curIndex,
+ slideToPrevContent,
+ slideToNextContent,
+ isFirstContentIndex,
+ isLastContentIndex,
+ childrenArray,
+ };
+};
+
+export default useSlider;
diff --git a/client/src/hooks/roadmap/useCollectRoadmapData.ts b/client/src/hooks/roadmap/useCollectRoadmapData.ts
index 2bfed9fce..d5b1658f1 100644
--- a/client/src/hooks/roadmap/useCollectRoadmapData.ts
+++ b/client/src/hooks/roadmap/useCollectRoadmapData.ts
@@ -1,6 +1,9 @@
import { DummyCategoryType } from '@/components/roadmapCreatePage/category/Category';
-import { DummyDifficultyType } from '@/components/roadmapCreatePage/difficulty/Difficulty';
-import { NodeImagesType, RoadmapValueType } from '@/myTypes/roadmap/internal';
+import {
+ DifficultyKeyType,
+ NodeImagesType,
+ RoadmapValueType,
+} from '@/myTypes/roadmap/internal';
import { getInvariantObjectKeys, invariantOf } from '@/utils/_common/invariantType';
import { useEffect, useState } from 'react';
import { useCreateRoadmap } from '../queries/roadmap';
@@ -27,7 +30,7 @@ export const useCollectRoadmapData = () => {
}));
};
- const getSelectedDifficulty = (difficulty: keyof DummyDifficultyType | null) => {
+ const getSelectedDifficulty = (difficulty: DifficultyKeyType | null) => {
setRoadmapValue((prev) => ({
...prev,
difficulty,
@@ -106,6 +109,7 @@ export const useCollectRoadmapData = () => {
if (isSumbited) {
createRoadmap(formData);
}
+ setIsSubmited(false);
}, [isSumbited]);
return {
diff --git a/client/src/hooks/roadmap/useCreateTag.ts b/client/src/hooks/roadmap/useCreateTag.ts
index 913ec80c8..28ee13847 100644
--- a/client/src/hooks/roadmap/useCreateTag.ts
+++ b/client/src/hooks/roadmap/useCreateTag.ts
@@ -6,8 +6,9 @@ export const useCreateTag = () => {
const ref = useRef(null);
const getAddedTagText = () => {
- if (ref.current === null) return;
- if (ref.current.value === '') return;
+ if (!ref.current?.value) return;
+
+ if (tags.includes(ref.current.value)) return;
setTags((prev) => {
return [...prev, ref.current?.value as string];
@@ -31,6 +32,10 @@ export const useCreateTag = () => {
return tags.length < TAG_LIMIT;
};
+ const checkIsAddCountMax = () => {
+ return tags.length < TAG_LIMIT - 1;
+ };
+
const deleteTag = (e: React.MouseEvent) => {
e.preventDefault();
@@ -40,5 +45,13 @@ export const useCreateTag = () => {
return prev.filter((tag) => tag !== target.value);
});
};
- return { tags, ref, addTagByButton, addTagByEnter, checkIsTagCountMax, deleteTag };
+ return {
+ tags,
+ ref,
+ addTagByButton,
+ addTagByEnter,
+ checkIsTagCountMax,
+ checkIsAddCountMax,
+ deleteTag,
+ };
};
diff --git a/client/src/myTypes/roadmap/internal.ts b/client/src/myTypes/roadmap/internal.ts
index aeb9a0fbe..76aba525a 100644
--- a/client/src/myTypes/roadmap/internal.ts
+++ b/client/src/myTypes/roadmap/internal.ts
@@ -1,6 +1,17 @@
import { DIFFICULTY_ICON_NAME } from '@constants/roadmap/difficulty';
import { CategoriesInfo } from '@constants/roadmap/category';
+export type DifficultyKeyType =
+ | 'VERY_EASY'
+ | 'EASY'
+ | 'NORMAL'
+ | 'DIFFICULT'
+ | 'VERY_DIFFICULT';
+
+export type DifficultyValueType = '매우쉬움' | '쉬움' | '보통' | '어려움' | '매우어려움';
+
+export type DifficultiesType = { [key in DifficultyKeyType]: DifficultyValueType };
+
export type CategoryType = {
id: keyof typeof CategoriesInfo;
name: string;
@@ -64,7 +75,7 @@ export type RoadmapValueType = {
title: null | string;
introduction: null | string;
content: null | string;
- difficulty: null | number;
+ difficulty: null | DifficultyKeyType;
requiredPeriod: null | string;
roadmapTags: { name: string }[];
roadmapNodes: RoadmapNodes[];
diff --git a/client/src/styles/GlobalStyle.tsx b/client/src/styles/GlobalStyle.tsx
index bc8f7b2d3..90134b3c2 100644
--- a/client/src/styles/GlobalStyle.tsx
+++ b/client/src/styles/GlobalStyle.tsx
@@ -60,13 +60,14 @@ const GlobalStyle = createGlobalStyle`
font-family: 'Noto Sans KR';
font-display: optional;
src: url(${require('../assets/fonts/NotoSansKR-Regular.woff')}) format('woff');
+ unicode-range: U+0020-007E;
}
@font-face {
font-family: 'Noto Sans';
font-display: optional;
src: url(${require('../assets/fonts/NotoSans-Regular.woff')}) format('woff');
- unicode-range: U+0041-005A, U+0061-007A;
+ unicode-range: U+0020-007E;
}
:root {
diff --git a/client/src/styles/media.ts b/client/src/styles/media.ts
index 1efe3875d..6bb9ba128 100644
--- a/client/src/styles/media.ts
+++ b/client/src/styles/media.ts
@@ -1,4 +1,4 @@
-import { css } from 'styled-components';
+import { RuleSet, css } from 'styled-components';
import BREAK_POINTS from '@constants/_common/breakPoints';
/*
@@ -17,20 +17,20 @@ import BREAK_POINTS from '@constants/_common/breakPoints';
*/
const media = {
- mobile: (styles: TemplateStringsArray) => css`
+ mobile: (styles: TemplateStringsArray | RuleSet