diff --git a/.storybook/provider.tsx b/.storybook/provider.tsx index 771a51ce..e0a1dc9d 100644 --- a/.storybook/provider.tsx +++ b/.storybook/provider.tsx @@ -3,6 +3,7 @@ import theme from '../src/styles/theme'; import GlobalStyle from '../src/styles/GlobalStyles'; import React, { ReactNode, useState } from 'react'; import { BrowserRouter } from 'react-router-dom'; +import { HelmetProvider } from 'react-helmet-async'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; interface ProviderProps { @@ -16,8 +17,10 @@ export const Provider = ({ children }: ProviderProps) => { return ( - - {children} + + + {children} + ); diff --git a/index.html b/index.html index ad5c4c98..8b8bb13d 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,11 @@ + + + + + 다루다(daruda) diff --git a/package.json b/package.json index 606a0022..d7ef485f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "lottie-react": "^2.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-helmet-async": "^2.0.5", "react-intersection-observer": "^9.15.1", "react-lottie": "^1.2.10", "react-lottie-player": "^2.1.0", diff --git a/public/og_img.png b/public/og_img.png new file mode 100644 index 00000000..75f2b576 Binary files /dev/null and b/public/og_img.png differ diff --git a/public/og_img1.png b/public/og_img1.png new file mode 100644 index 00000000..76779d4e Binary files /dev/null and b/public/og_img1.png differ diff --git a/public/og_img2.png b/public/og_img2.png new file mode 100644 index 00000000..0a4b9be4 Binary files /dev/null and b/public/og_img2.png differ diff --git a/src/components/common/title/Title.tsx b/src/components/common/title/Title.tsx new file mode 100644 index 00000000..f8fc606e --- /dev/null +++ b/src/components/common/title/Title.tsx @@ -0,0 +1,12 @@ +import { Helmet } from 'react-helmet-async'; + +const Title = ({ title, tool }: { title: string; tool?: string }) => { + return ( + + {title} + + + ); +}; + +export default Title; diff --git a/src/components/layout/MyPageLayout.tsx b/src/components/layout/MyPageLayout.tsx index 4b543e4f..cecd9344 100644 --- a/src/components/layout/MyPageLayout.tsx +++ b/src/components/layout/MyPageLayout.tsx @@ -1,5 +1,6 @@ import Footer from '@components/footer/Footer'; import Header from '@components/header/Header'; +import Title from '@components/title/Title'; import { HEADER_STATE, HeaderState } from '@constants/headerState'; import styled from '@emotion/styled'; import MyPageTab from '@pages/myPage/components/tab/MyPageTab'; @@ -14,6 +15,7 @@ const MyPageLayout = () => { return ( <>
+ <MyPageContainer> <Outlet /> </MyPageContainer> diff --git a/src/main.tsx b/src/main.tsx index a88017a0..4aee8c4b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import React from 'react'; import ReactDOM from 'react-dom/client'; +import { HelmetProvider } from 'react-helmet-async'; import App from './App'; @@ -15,10 +16,12 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( <GlobalStyle /> <ThemeProvider theme={theme}> <QueryClientProvider client={queryClient}> - <App /> - <div style={{ fontSize: '1.6rem' }}> - <ReactQueryDevtools initialIsOpen={false} /> - </div> + <HelmetProvider> + <App /> + <div style={{ fontSize: '1.6rem' }}> + <ReactQueryDevtools initialIsOpen={false} /> + </div> + </HelmetProvider> </QueryClientProvider> </ThemeProvider> </React.StrictMode>, diff --git a/src/pages/CommunityDetail/CommunityDetail.tsx b/src/pages/CommunityDetail/CommunityDetail.tsx index d1a17eae..8a9a9cf5 100644 --- a/src/pages/CommunityDetail/CommunityDetail.tsx +++ b/src/pages/CommunityDetail/CommunityDetail.tsx @@ -1,6 +1,7 @@ import { IcCommentGray24, IcBookmark } from '@assets/svgs'; import SquareButton from '@components/button/squareButton/SquareButton'; import Card from '@components/postCard/PostCard'; +import Title from '@components/title/Title'; import { handleScrollDown } from '@utils'; import { useRef, useEffect, useState } from 'react'; import { useInView } from 'react-intersection-observer'; @@ -37,6 +38,7 @@ const CommunityDetail = () => { const comments = CommentData?.pages.flatMap((page) => page.commentList) || []; return ( <> + <Title title={data?.title as string} /> <S.PageWrapper> <S.PageHeader> <h1>글 상세보기</h1> diff --git a/src/pages/community/Community.tsx b/src/pages/community/Community.tsx index 65e4d4df..3ffd1133 100644 --- a/src/pages/community/Community.tsx +++ b/src/pages/community/Community.tsx @@ -1,6 +1,7 @@ import { IcPlusWhite20, IcChevron } from '@assets/svgs'; import ToolListBanner from '@components/banner/ToolListBanner'; import CircleButton from '@components/button/circleButton/CircleButton'; +import Title from '@components/title/Title'; import { handleScrollUp } from '@utils'; import { useEffect, useState } from 'react'; import { useInView } from 'react-intersection-observer'; @@ -31,24 +32,27 @@ const Community = () => { setIsNoTopic(toolId === null); }; return ( - <S.CommunityWrapper> - <Banner /> - <S.CommunityContainer> - <ToolListBanner forCommunity={true} onToolSelect={handleToolSelect} /> - <S.CardList> - {postList?.map((post) => <Card key={`community-post-${post.boardId}`} post={post} />)} - {hasNextPage ? <div ref={ref} /> : null} - </S.CardList> - </S.CommunityContainer> - <S.FollowingBtns> - <CircleButton size="small" shadow={true} icon={<IcPlusWhite20 />}> - 글쓰기 - </CircleButton> - <S.TopBtn type="button" onClick={handleScrollUp}> - <IcChevron /> - </S.TopBtn> - </S.FollowingBtns> - </S.CommunityWrapper> + <> + <Title title="커뮤니티" /> + <S.CommunityWrapper> + <Banner /> + <S.CommunityContainer> + <ToolListBanner forCommunity={true} onToolSelect={handleToolSelect} /> + <S.CardList> + {postList?.map((post) => <Card key={`community-post-${post.boardId}`} post={post} />)} + {hasNextPage ? <div ref={ref} /> : null} + </S.CardList> + </S.CommunityContainer> + <S.FollowingBtns> + <CircleButton size="small" shadow={true} icon={<IcPlusWhite20 />}> + 글쓰기 + </CircleButton> + <S.TopBtn type="button" onClick={handleScrollUp}> + <IcChevron /> + </S.TopBtn> + </S.FollowingBtns> + </S.CommunityWrapper> + </> ); }; diff --git a/src/pages/communityWrite/CommunityWrite.tsx b/src/pages/communityWrite/CommunityWrite.tsx index 20141533..09212f9b 100644 --- a/src/pages/communityWrite/CommunityWrite.tsx +++ b/src/pages/communityWrite/CommunityWrite.tsx @@ -1,5 +1,6 @@ import ToolListBanner from '@components/banner/ToolListBanner'; import CircleButton from '@components/button/circleButton/CircleButton'; +import Title from '@components/title/Title'; import Toast from '@components/toast/Toast'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -47,29 +48,32 @@ const CommunityWrite = () => { }; return ( - <S.WriteWrapper> - <S.WriteTitle>글쓰기</S.WriteTitle> - <S.WriteContainer> - <S.WriteBox> - <WritingTitle setTitle={setTitle} /> - <WritingBody setBody={setBody} /> - <WritingImg onImageUpload={setImages} /> - </S.WriteBox> - <S.SideBanner> - <ToolListBanner onToolSelect={handleToolSelect} /> - <CircleButton onClick={handlePostSubmit} size="large" disabled={isButtonDisabled}> - 글 게시하기 - </CircleButton> - </S.SideBanner> - </S.WriteContainer> - {isToastVisible && ( - <S.ToastBox> - <Toast isVisible={true} isWarning={true}> - {toastMessage} - </Toast> - </S.ToastBox> - )} - </S.WriteWrapper> + <> + <Title title="글쓰기" /> + <S.WriteWrapper> + <S.WriteTitle>글쓰기</S.WriteTitle> + <S.WriteContainer> + <S.WriteBox> + <WritingTitle setTitle={setTitle} /> + <WritingBody setBody={setBody} /> + <WritingImg onImageUpload={setImages} /> + </S.WriteBox> + <S.SideBanner> + <ToolListBanner onToolSelect={handleToolSelect} /> + <CircleButton onClick={handlePostSubmit} size="large" disabled={isButtonDisabled}> + 글 게시하기 + </CircleButton> + </S.SideBanner> + </S.WriteContainer> + {isToastVisible && ( + <S.ToastBox> + <Toast isVisible={true} isWarning={true}> + {toastMessage} + </Toast> + </S.ToastBox> + )} + </S.WriteWrapper> + </> ); }; diff --git a/src/pages/login/KakaoAuth.tsx b/src/pages/login/KakaoAuth.tsx index 0ef501e7..d588b146 100644 --- a/src/pages/login/KakaoAuth.tsx +++ b/src/pages/login/KakaoAuth.tsx @@ -1,24 +1,28 @@ import { ImgDarudalogo40 } from '@assets/svgs'; +import Title from '@components/title/Title'; import SvgKakaoVector from './assets/KakaoVector'; import * as S from './KakaoAuth.styled'; const KakaoAuth = () => { return ( - <S.LogintWrapper> - <S.Container> - <S.LogoSection> - <ImgDarudalogo40 width={407} height={120} /> - </S.LogoSection> - <span>모든 대학생이 찾는 솔루션, 다루다에서 만나보세요.</span> - <S.LoginButtonContainer> - <S.LoginButton aria-label="카카오 로그인"> - <SvgKakaoVector /> - <p>카카오 로그인</p> - </S.LoginButton> - </S.LoginButtonContainer> - </S.Container> - </S.LogintWrapper> + <> + <Title title="로그인" /> + <S.LogintWrapper> + <S.Container> + <S.LogoSection> + <ImgDarudalogo40 width={407} height={120} /> + </S.LogoSection> + <span>모든 대학생이 찾는 솔루션, 다루다에서 만나보세요.</span> + <S.LoginButtonContainer> + <S.LoginButton aria-label="카카오 로그인"> + <SvgKakaoVector /> + <p>카카오 로그인</p> + </S.LoginButton> + </S.LoginButtonContainer> + </S.Container> + </S.LogintWrapper> + </> ); }; diff --git a/src/pages/signUp/SignUp.tsx b/src/pages/signUp/SignUp.tsx index 473b9acc..c84693a2 100644 --- a/src/pages/signUp/SignUp.tsx +++ b/src/pages/signUp/SignUp.tsx @@ -1,6 +1,7 @@ import { ImgModalcheck } from '@assets/svgs'; import CircleButton from '@components/button/circleButton/CircleButton'; import { AlterModal } from '@components/modal'; +import Title from '@components/title/Title'; import React, { useState } from 'react'; import AffiliationBtn from './components/affiliationButton/AffiliationBtn'; @@ -43,50 +44,53 @@ const SignUp = () => { }; return ( - <S.SignUpWrapper> - <S.Container> - <S.LeftContainer> - <S.LeftBox> - <S.TitleBox> - 쉬운 대학생활, <br /> - 앞으로 한 걸음 남았어요. - </S.TitleBox> - <S.CommentBox> - 공부, 과제, 팀플, 동아리, 대외활동 <br /> - 복잡하고 어렵기만 했던 툴에 대한 고민은 <br /> - 다루다가 해결해 드릴게요. - </S.CommentBox> - </S.LeftBox> - </S.LeftContainer> - <S.RightContainer> - <h1>회원가입</h1> - <S.AffiliationBox> - <h2>소속을 선택해주세요.</h2> - <S.AffiliationBtnBox> - {AFFILIATION_OPTIONS.map((label) => ( - <AffiliationBtn - key={label} - label={label} - isSelected={selectedAffiliation === label} - onClick={() => setSelectedAffiliation(label)} - /> - ))} - </S.AffiliationBtnBox> - </S.AffiliationBox> - <S.NicknameInputBox> - {/* TODO: 중복확인 상태에 따른 로직 구현 */} - <NamingInput value={nickname} onChange={handleNicknameChange} /> - </S.NicknameInputBox> - <S.SignUpBtn> - {/* TODO: 중복확인 되었을 때만 작동되도록 */} - <CircleButton size="mini" disabled={!isCircleBtnActive} onClick={handleCircleBtnClick}> - 회원가입 하기 - </CircleButton> - </S.SignUpBtn> - </S.RightContainer> - <AlterModal {...modalProps} /> - </S.Container> - </S.SignUpWrapper> + <> + <Title title="회원가입" /> + <S.SignUpWrapper> + <S.Container> + <S.LeftContainer> + <S.LeftBox> + <S.TitleBox> + 쉬운 대학생활, <br /> + 앞으로 한 걸음 남았어요. + </S.TitleBox> + <S.CommentBox> + 공부, 과제, 팀플, 동아리, 대외활동 <br /> + 복잡하고 어렵기만 했던 툴에 대한 고민은 <br /> + 다루다가 해결해 드릴게요. + </S.CommentBox> + </S.LeftBox> + </S.LeftContainer> + <S.RightContainer> + <h1>회원가입</h1> + <S.AffiliationBox> + <h2>소속을 선택해주세요.</h2> + <S.AffiliationBtnBox> + {AFFILIATION_OPTIONS.map((label) => ( + <AffiliationBtn + key={label} + label={label} + isSelected={selectedAffiliation === label} + onClick={() => setSelectedAffiliation(label)} + /> + ))} + </S.AffiliationBtnBox> + </S.AffiliationBox> + <S.NicknameInputBox> + {/* TODO: 중복확인 상태에 따른 로직 구현 */} + <NamingInput value={nickname} onChange={handleNicknameChange} /> + </S.NicknameInputBox> + <S.SignUpBtn> + {/* TODO: 중복확인 되었을 때만 작동되도록 */} + <CircleButton size="mini" disabled={!isCircleBtnActive} onClick={handleCircleBtnClick}> + 회원가입 하기 + </CircleButton> + </S.SignUpBtn> + </S.RightContainer> + <AlterModal {...modalProps} /> + </S.Container> + </S.SignUpWrapper> + </> ); }; diff --git a/src/pages/toolDetail/ToolDetail.tsx b/src/pages/toolDetail/ToolDetail.tsx index 3c02885a..51a6d481 100644 --- a/src/pages/toolDetail/ToolDetail.tsx +++ b/src/pages/toolDetail/ToolDetail.tsx @@ -1,4 +1,5 @@ import Spacing from '@components/spacing/Spacing'; +import Title from '@components/title/Title'; import { useRef } from 'react'; import BreadCrumb from './components/breadcrumb/BreadCrumb'; @@ -29,38 +30,41 @@ const ToolDetail = () => { }; return ( - <S.ToolDetailWrapper> - <Spacing size={'1.8'} /> - <BreadCrumb activeTopic={DETAIL_RESPONSE.data.category} activeTool={DETAIL_RESPONSE.data.toolMainName} /> - <Spacing size={'1.8'} /> - <ToolInfoCard toolData={DETAIL_RESPONSE.data} /> + <> + <Title title={DETAIL_RESPONSE.data.toolMainName} tool={DETAIL_RESPONSE.data.toolMainName} /> + <S.ToolDetailWrapper> + <Spacing size={'1.8'} /> + <BreadCrumb activeTopic={DETAIL_RESPONSE.data.category} activeTool={DETAIL_RESPONSE.data.toolMainName} /> + <Spacing size={'1.8'} /> + <ToolInfoCard toolData={DETAIL_RESPONSE.data} /> - <Spacing size={'1'} /> + <Spacing size={'1'} /> - <S.ToolDetailContainer> - <section> - <S.ToolDetailBox> - <ToolIntro - ref={ToolIntroRef} - toolImage={DETAIL_RESPONSE.data.images} - activeTool={DETAIL_RESPONSE.data.toolMainName} - description={DETAIL_RESPONSE.data.detailDescription} - /> - <CoreFeature ref={CoreFeatureRef} /> - <ReferenceVideo ref={ReferenceVideoRef} /> - <PlanBox ref={PlanBoxRef} /> + <S.ToolDetailContainer> + <section> + <S.ToolDetailBox> + <ToolIntro + ref={ToolIntroRef} + toolImage={DETAIL_RESPONSE.data.images} + activeTool={DETAIL_RESPONSE.data.toolMainName} + description={DETAIL_RESPONSE.data.detailDescription} + /> + <CoreFeature ref={CoreFeatureRef} /> + <ReferenceVideo ref={ReferenceVideoRef} /> + <PlanBox ref={PlanBoxRef} /> + <Spacing size={'1'} /> + </S.ToolDetailBox> <Spacing size={'1'} /> - </S.ToolDetailBox> - <Spacing size={'1'} /> - <S.ToolCommunityBox> - <ToolCommunity cards={COMMUNITY_RESPONSE.data.contents} ref={ToolCommunityRef} /> - </S.ToolCommunityBox> - <Spacing size={'7.2'} /> - </section> - <Sidewing sectionRefs={sectionRefs} /> - </S.ToolDetailContainer> - </S.ToolDetailWrapper> + <S.ToolCommunityBox> + <ToolCommunity cards={COMMUNITY_RESPONSE.data.contents} ref={ToolCommunityRef} /> + </S.ToolCommunityBox> + <Spacing size={'7.2'} /> + </section> + <Sidewing sectionRefs={sectionRefs} /> + </S.ToolDetailContainer> + </S.ToolDetailWrapper> + </> ); }; diff --git a/yarn.lock b/yarn.lock index c19a6f80..f3f18ea8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4073,6 +4073,13 @@ internal-slot@^1.1.0: hasown "^2.0.2" side-channel "^1.1.0" +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + is-arguments@^1.0.4: version "1.2.0" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b" @@ -4555,7 +4562,7 @@ log-update@^6.1.0: strip-ansi "^7.1.0" wrap-ansi "^9.0.0" -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -5171,11 +5178,20 @@ react-docgen@^7.0.0: loose-envify "^1.1.0" scheduler "^0.23.2" -react-fast-compare@^3.0.1: +react-fast-compare@^3.0.1, react-fast-compare@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== +react-helmet-async@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-2.0.5.tgz#cfc70cd7bb32df7883a8ed55502a1513747223ec" + integrity sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg== + dependencies: + invariant "^2.2.4" + react-fast-compare "^3.2.2" + shallowequal "^1.1.0" + react-intersection-observer@^9.15.1: version "9.15.1" resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.15.1.tgz#5866b6fdfef24be9f288b74b35b773f63d90c3eb" @@ -5536,6 +5552,11 @@ set-proto@^1.0.0: es-errors "^1.3.0" es-object-atoms "^1.0.0" +shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"