├── src ├── pages │ ├── Edit │ │ ├── style.js │ │ └── index.jsx │ ├── Post │ │ ├── style.js │ │ └── index.jsx │ ├── History │ │ ├── style.js │ │ └── index.jsx │ ├── HistoryDetail │ │ ├── style.js │ │ └── index.jsx │ ├── NoticeDetail │ │ ├── style.js │ │ └── index.jsx │ ├── NotFound │ │ ├── style.js │ │ └── index.jsx │ ├── Promotion │ │ └── index.jsx │ ├── InquiryWrite │ │ └── index.jsx │ ├── Club │ │ ├── style.js │ │ └── index.jsx │ ├── Event │ │ ├── style.js │ │ └── index.jsx │ ├── Schedule │ │ ├── style.js │ │ └── index.jsx │ ├── Teacher │ │ ├── style.js │ │ └── index.jsx │ ├── Major │ │ ├── style.js │ │ └── index.jsx │ ├── Project │ │ ├── style.js │ │ └── index.jsx │ ├── Student │ │ ├── style.js │ │ └── index.jsx │ ├── Notice │ │ ├── style.js │ │ └── index.jsx │ ├── NoticeWrite │ │ └── index.jsx │ ├── EditNotice │ │ └── index.jsx │ ├── Inquiry │ │ ├── style.js │ │ └── index.jsx │ ├── Role │ │ ├── style.js │ │ └── index.jsx │ ├── index.js │ ├── BoardDetail │ │ └── index.jsx │ ├── InquiryDetail │ │ └── index.jsx │ └── Main │ │ └── style.js ├── assets │ ├── SchoolSong.jsx │ ├── data │ │ └── RoleData.js │ ├── DivideLineIcon.jsx │ ├── Toggle.jsx │ ├── QuoteIcon.jsx │ ├── InclineIcon.jsx │ ├── DrawIcon.jsx │ ├── ImageIcon.jsx │ ├── X.jsx │ ├── LinkIcon.jsx │ ├── BoldIcon.jsx │ ├── Arrow.jsx │ ├── Notice.jsx │ ├── Search.jsx │ ├── GAuthLogo.jsx │ ├── Etc.jsx │ ├── index.jsx │ ├── ScrollButton.jsx │ ├── LoginLogo.jsx │ ├── AlarmIcon.jsx │ ├── H1Icon.jsx │ ├── CodeIcon.jsx │ ├── H4Icon.jsx │ ├── School.jsx │ ├── Logout.jsx │ ├── H2Icon.jsx │ └── H3Icon.jsx ├── imgs │ ├── RoleImg1.png │ ├── RoleImg2.png │ ├── SchoolImg.webp │ ├── promotion.webp │ ├── SchoolSong.webp │ ├── PromotionCom.webp │ ├── openToggle.svg │ └── closeToggle.svg ├── components │ ├── Explanation │ │ ├── style.js │ │ └── index.jsx │ ├── Footer │ │ ├── index.jsx │ │ └── style.js │ ├── WriteBox │ │ ├── PreviewWriteBox │ │ │ ├── index.jsx │ │ │ └── MarkdownConverter │ │ │ │ └── index.jsx │ │ ├── EditWriteBox │ │ │ └── style.js │ │ ├── style.js │ │ └── index.jsx │ ├── InquiryItem │ │ ├── index.jsx │ │ └── sytle.js │ ├── ContentsButton │ │ └── index.jsx │ ├── HistoryItem │ │ ├── style.js │ │ └── index.jsx │ ├── ScrollButton │ │ ├── style.js │ │ └── index.jsx │ ├── Button │ │ ├── style.js │ │ └── index.jsx │ ├── HistoryDetailItem │ │ ├── style.js │ │ └── index.jsx │ ├── Graph │ │ ├── index.jsx │ │ └── style.js │ ├── InquiryModal │ │ ├── index.jsx │ │ └── style.js │ ├── BoardDetail │ │ ├── style.js │ │ └── index.jsx │ ├── NoticeDetail │ │ ├── index.jsx │ │ └── style.js │ ├── Header │ │ ├── dropMenu │ │ │ ├── style.js │ │ │ └── index.jsx │ │ └── style.js │ ├── Logout │ │ ├── index.jsx │ │ └── style.js │ ├── Detail │ │ ├── index.jsx │ │ └── style.js │ ├── PromotionPage │ │ ├── index.jsx │ │ └── style.js │ ├── Refusal │ │ ├── index.jsx │ │ └── style.js │ ├── PageContainer │ │ ├── style.js │ │ └── index.jsx │ ├── InquiryDetailItem │ │ ├── style.js │ │ └── index.jsx │ ├── RecentModified │ │ ├── style.js │ │ └── index.jsx │ ├── index.js │ ├── WriteOption │ │ └── style.js │ ├── InquiryWrite │ │ ├── style.js │ │ └── index.jsx │ ├── EditNotice │ │ ├── style.js │ │ └── index.jsx │ ├── NoticeWrite │ │ ├── style.js │ │ └── index.jsx │ └── EditWrite │ │ ├── style.js │ │ └── index.jsx ├── lib │ ├── GAuthLoginUrl.js │ ├── token.js │ ├── GetRole.js │ ├── Observable.js │ └── mainPageData.js ├── setupTests.js ├── apis │ ├── EnvConfig.js │ ├── index.js │ └── TokenManager.js ├── reportWebVitals.js ├── store │ ├── uuid.js │ ├── index.js │ └── reissue.js ├── Hooks │ ├── index.js │ ├── useHistory.js │ ├── useContent.js │ ├── useBoard.js │ ├── useDelete.js │ ├── useSearchList.js │ ├── useMail.js │ ├── useFile.js │ ├── useInquiry.js │ ├── useUpload.js │ ├── useLogin.js │ ├── useMarkdown.js │ ├── useFetch.js │ ├── useEdit.js │ └── useNotice.js ├── index.js ├── App.js └── router │ └── index.jsx ├── public ├── favicon │ ├── apple-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── ms-icon-70x70.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── android-icon-192x192.png │ ├── apple-icon-precomposed.png │ ├── browserconfig.xml │ └── manifest.json ├── fonts │ ├── woff │ │ ├── Pretendard-Bold.woff │ │ ├── Pretendard-Thin.woff │ │ ├── Pretendard-Black.woff │ │ ├── Pretendard-Light.woff │ │ ├── Pretendard-Medium.woff │ │ ├── Pretendard-ExtraBold.woff │ │ ├── Pretendard-Regular.woff │ │ ├── Pretendard-SemiBold.woff │ │ └── Pretendard-ExtraLight.woff │ └── woff2 │ │ ├── Pretendard-Bold.woff2 │ │ ├── Pretendard-Thin.woff2 │ │ ├── Pretendard-Black.woff2 │ │ ├── Pretendard-Light.woff2 │ │ ├── Pretendard-Medium.woff2 │ │ ├── Pretendard-Regular.woff2 │ │ ├── Pretendard-ExtraBold.woff2 │ │ ├── Pretendard-SemiBold.woff2 │ │ └── Pretendard-ExtraLight.woff2 └── index.html ├── .gitignore ├── .prettierrc ├── package.json └── README.md /src/pages/Edit/style.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/Post/style.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/SchoolSong.jsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/History/style.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/HistoryDetail/style.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/NoticeDetail/style.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/imgs/RoleImg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/src/imgs/RoleImg1.png -------------------------------------------------------------------------------- /src/imgs/RoleImg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/src/imgs/RoleImg2.png -------------------------------------------------------------------------------- /src/imgs/SchoolImg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/src/imgs/SchoolImg.webp -------------------------------------------------------------------------------- /src/imgs/promotion.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/src/imgs/promotion.webp -------------------------------------------------------------------------------- /src/imgs/SchoolSong.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/src/imgs/SchoolSong.webp -------------------------------------------------------------------------------- /src/imgs/PromotionCom.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/src/imgs/PromotionCom.webp -------------------------------------------------------------------------------- /public/favicon/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/apple-icon.png -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /public/favicon/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/favicon/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/favicon/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/favicon/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/favicon/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/android-icon-36x36.png -------------------------------------------------------------------------------- /public/favicon/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/android-icon-48x48.png -------------------------------------------------------------------------------- /public/favicon/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/android-icon-72x72.png -------------------------------------------------------------------------------- /public/favicon/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/android-icon-96x96.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/fonts/woff/Pretendard-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff/Pretendard-Bold.woff -------------------------------------------------------------------------------- /public/fonts/woff/Pretendard-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff/Pretendard-Thin.woff -------------------------------------------------------------------------------- /public/favicon/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/android-icon-192x192.png -------------------------------------------------------------------------------- /public/fonts/woff/Pretendard-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff/Pretendard-Black.woff -------------------------------------------------------------------------------- /public/fonts/woff/Pretendard-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff/Pretendard-Light.woff -------------------------------------------------------------------------------- /public/fonts/woff/Pretendard-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff/Pretendard-Medium.woff -------------------------------------------------------------------------------- /public/fonts/woff2/Pretendard-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff2/Pretendard-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/woff2/Pretendard-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff2/Pretendard-Thin.woff2 -------------------------------------------------------------------------------- /src/assets/data/RoleData.js: -------------------------------------------------------------------------------- 1 | export const RoleData = { 2 | TOKEN: { 3 | ROLE_ADMIN: "관리자", 4 | ROLE_STUDENT: "사용자" 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/favicon/apple-icon-precomposed.png -------------------------------------------------------------------------------- /public/fonts/woff/Pretendard-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff/Pretendard-ExtraBold.woff -------------------------------------------------------------------------------- /public/fonts/woff/Pretendard-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff/Pretendard-Regular.woff -------------------------------------------------------------------------------- /public/fonts/woff/Pretendard-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff/Pretendard-SemiBold.woff -------------------------------------------------------------------------------- /public/fonts/woff2/Pretendard-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff2/Pretendard-Black.woff2 -------------------------------------------------------------------------------- /public/fonts/woff2/Pretendard-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff2/Pretendard-Light.woff2 -------------------------------------------------------------------------------- /public/fonts/woff2/Pretendard-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff2/Pretendard-Medium.woff2 -------------------------------------------------------------------------------- /public/fonts/woff2/Pretendard-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff2/Pretendard-Regular.woff2 -------------------------------------------------------------------------------- /public/fonts/woff/Pretendard-ExtraLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff/Pretendard-ExtraLight.woff -------------------------------------------------------------------------------- /public/fonts/woff2/Pretendard-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff2/Pretendard-ExtraBold.woff2 -------------------------------------------------------------------------------- /public/fonts/woff2/Pretendard-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff2/Pretendard-SemiBold.woff2 -------------------------------------------------------------------------------- /public/fonts/woff2/Pretendard-ExtraLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gmu-Wiki/Gmu-WiKi-Front-V2/HEAD/public/fonts/woff2/Pretendard-ExtraLight.woff2 -------------------------------------------------------------------------------- /src/components/Explanation/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Explanation = styled.div` 4 | width: 100%; 5 | border-top: 1px solid #d9d9d9; 6 | `; 7 | -------------------------------------------------------------------------------- /src/lib/GAuthLoginUrl.js: -------------------------------------------------------------------------------- 1 | import EnvConfig from "../apis/EnvConfig"; 2 | 3 | export const gauthLoginUri = `https://gauth-msg.vercel.app/login?client_id=${EnvConfig.CLIENTID}&redirect_uri=${EnvConfig.REDIRECTURL}`; 4 | -------------------------------------------------------------------------------- /src/lib/token.js: -------------------------------------------------------------------------------- 1 | export const accessToken = "GMUWIKI_accessToken"; 2 | export const refreshToken = "GMUWIKI_refreshToken"; 3 | export const accessExp = "GMUWIKI_accessExp"; 4 | export const refreshExp = "GMUWIKI_refreshExp"; -------------------------------------------------------------------------------- /src/pages/NotFound/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const NotFoundCotainer = styled.div` 4 | height: 775px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | `; 9 | -------------------------------------------------------------------------------- /src/pages/Promotion/index.jsx: -------------------------------------------------------------------------------- 1 | import { useLogin } from "../../Hooks"; 2 | import * as C from "../../components"; 3 | 4 | function Promotion() { 5 | useLogin(); 6 | return ; 7 | } 8 | 9 | export default Promotion; 10 | -------------------------------------------------------------------------------- /src/components/Explanation/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./style"; 3 | 4 | function Explanation({ children }) { 5 | return {children}; 6 | } 7 | 8 | export default Explanation; 9 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/pages/Post/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as C from "../../components"; 3 | 4 | function Post() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | 12 | export default Post; 13 | -------------------------------------------------------------------------------- /public/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /src/apis/EnvConfig.js: -------------------------------------------------------------------------------- 1 | const EnvConfig = { 2 | CLIENTID: process.env.REACT_APP_CLIENT_ID, 3 | CLIENTSECRET: process.env.REACT_APP_CLIENT_SECRET, 4 | REDIRECTURL: process.env.REACT_APP_REDIRECT_URI, 5 | GMUWIKI_SERVER_URL: process.env.REACT_APP_GMUWIKI_SERVER_URL, 6 | }; 7 | 8 | export default EnvConfig; 9 | -------------------------------------------------------------------------------- /src/components/Footer/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./style"; 3 | 4 | function Footer() { 5 | return ( 6 | 7 | Copyright 2023. &mpersand All rights reserved. 8 | 9 | ); 10 | } 11 | 12 | export default Footer; 13 | -------------------------------------------------------------------------------- /src/components/WriteBox/PreviewWriteBox/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as C from "../../index"; 3 | 4 | function PreviewWriteBox({ content }) { 5 | 6 | return ( 7 | <> 8 | 9 | 10 | ); 11 | } 12 | 13 | export default PreviewWriteBox; -------------------------------------------------------------------------------- /src/pages/NotFound/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as I from "../../assets"; 3 | import * as S from "./style"; 4 | 5 | function NotFound() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default NotFound; 14 | -------------------------------------------------------------------------------- /src/pages/InquiryWrite/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as C from "../../components"; 3 | 4 | const History = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default History; 13 | -------------------------------------------------------------------------------- /src/components/Footer/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | export const FooterConainer = styled.div` 3 | width: 100%; 4 | height: 70px; 5 | background-color: #dddddd; 6 | color: #636363; 7 | font-size: 1rem; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | `; 12 | -------------------------------------------------------------------------------- /src/components/InquiryItem/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./sytle"; 3 | 4 | export default function InquiryItem({ children }) { 5 | return ( 6 | 7 | {children} 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/DivideLineIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function DivideLineIcon() { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | } 16 | 17 | export default DivideLineIcon; 18 | -------------------------------------------------------------------------------- /src/assets/Toggle.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Toggle() { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | } 16 | 17 | export default Toggle; 18 | -------------------------------------------------------------------------------- /src/pages/Club/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Box = styled.div` 4 | display: flex; 5 | margin-bottom: 16px; 6 | align-items: center; 7 | margin-left: 26px; 8 | `; 9 | 10 | export const Title = styled.p` 11 | font-size: 1rem; 12 | font-weight: 600; 13 | color: #007eff; 14 | cursor: pointer; 15 | line-height: 20px; 16 | margin-right: auto; 17 | `; 18 | -------------------------------------------------------------------------------- /src/pages/Event/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Box = styled.div` 4 | display: flex; 5 | margin-bottom: 16px; 6 | align-items: center; 7 | margin-left: 26px; 8 | `; 9 | 10 | export const Title = styled.p` 11 | font-size: 1rem; 12 | font-weight: 600; 13 | color: #007eff; 14 | cursor: pointer; 15 | line-height: 20px; 16 | margin-right: auto; 17 | `; 18 | -------------------------------------------------------------------------------- /src/pages/Schedule/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Box = styled.div` 4 | display: flex; 5 | margin-bottom: 16px; 6 | align-items: center; 7 | margin-left: 26px; 8 | `; 9 | 10 | export const Title = styled.p` 11 | font-size: 1rem; 12 | font-weight: 600; 13 | color: #007eff; 14 | cursor: pointer; 15 | line-height: 20px; 16 | margin-right: auto; 17 | `; 18 | -------------------------------------------------------------------------------- /src/pages/Teacher/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Box = styled.div` 4 | display: flex; 5 | margin-bottom: 16px; 6 | align-items: center; 7 | margin-left: 26px; 8 | `; 9 | 10 | export const Title = styled.p` 11 | font-size: 1rem; 12 | font-weight: 600; 13 | color: #007eff; 14 | cursor: pointer; 15 | line-height: 20px; 16 | margin-right: auto; 17 | `; 18 | -------------------------------------------------------------------------------- /src/components/ContentsButton/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as C from "../../components"; 3 | 4 | export default function ContentsButton({ children }) { 5 | return ( 6 | 13 | {children} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/Major/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const MajorBox = styled.div` 4 | display: flex; 5 | margin-bottom: 16px; 6 | align-items: center; 7 | margin-left: 26px; 8 | `; 9 | 10 | export const MajorTitle = styled.p` 11 | font-size: 1rem; 12 | font-weight: 600; 13 | color: #007eff; 14 | cursor: pointer; 15 | line-height: 20px; 16 | margin-right: auto; 17 | `; 18 | -------------------------------------------------------------------------------- /src/pages/Project/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ProjectBox = styled.div` 4 | display: flex; 5 | margin-bottom: 16px; 6 | align-items: center; 7 | margin-left: 26px; 8 | `; 9 | 10 | export const ProjectTitle = styled.p` 11 | font-size: 1rem; 12 | font-weight: 600; 13 | color: #007eff; 14 | cursor: pointer; 15 | line-height: 40px; 16 | margin-right: auto; 17 | `; -------------------------------------------------------------------------------- /src/pages/Student/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StudentBox = styled.div` 4 | display: flex; 5 | margin-bottom: 16px; 6 | align-items: center; 7 | margin-left: 26px; 8 | `; 9 | 10 | export const StudentTitle = styled.p` 11 | font-size: 1rem; 12 | font-weight: 600; 13 | color: #007eff; 14 | cursor: pointer; 15 | line-height: 20px; 16 | margin-right: auto; 17 | `; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env 26 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/assets/QuoteIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function QuoteIcon() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default QuoteIcon; 21 | -------------------------------------------------------------------------------- /src/assets/InclineIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function InclineIcon() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default InclineIcon; 21 | -------------------------------------------------------------------------------- /src/store/uuid.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = ""; 4 | const uuidSlice = createSlice({ 5 | name: "uuid", 6 | initialState, 7 | reducers: { 8 | setUuid: (state, action) => { 9 | state = action.payload; 10 | return state; 11 | }, 12 | removeUuid: () => { 13 | return initialState; 14 | }, 15 | }, 16 | }); 17 | 18 | export const { setUuid, removeUuid } = uuidSlice.actions; 19 | 20 | export default uuidSlice; 21 | -------------------------------------------------------------------------------- /src/components/HistoryItem/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Box = styled.div` 4 | display: flex; 5 | margin-bottom: 16px; 6 | align-items: center; 7 | margin-left: 26px; 8 | `; 9 | 10 | export const Day = styled.p` 11 | font-size: 1rem; 12 | font-weight: 600; 13 | color: #007eff; 14 | cursor: pointer; 15 | line-height: 40px; 16 | margin-right: 20px; 17 | `; 18 | 19 | export const Name = styled.p` 20 | font-size: 0.8rem; 21 | color: #999999; 22 | `; 23 | -------------------------------------------------------------------------------- /src/lib/GetRole.js: -------------------------------------------------------------------------------- 1 | import jwtDecode from "jwt-decode"; 2 | import TokenManager from "../apis/TokenManager"; 3 | import { RoleData } from "../assets/data/RoleData"; 4 | 5 | const GetRole = () => { 6 | const tokenManager = new TokenManager(); 7 | const accessToken = tokenManager.accessToken; 8 | 9 | if (!accessToken) return ""; 10 | 11 | const userTable = jwtDecode(accessToken); 12 | 13 | const setRole = RoleData.TOKEN[userTable.authority]; 14 | 15 | return setRole; 16 | }; 17 | 18 | export default GetRole; 19 | -------------------------------------------------------------------------------- /src/pages/Edit/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as C from "../../components"; 3 | import { useContent } from "../../Hooks"; 4 | import { useParams } from "react-router-dom"; 5 | 6 | const Edit = () => { 7 | const { id } = useParams(); 8 | const state = useContent({ id }); 9 | 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default Edit; 18 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "none", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false, 12 | "arrowParens": "avoid", 13 | "requirePragma": false, 14 | "insertPragma": false, 15 | "proseWrap": "preserve", 16 | "htmlWhitespaceSensitivity": "css", 17 | "vueIndentScriptAndStyle": false, 18 | "endOfLine": "lf" 19 | } 20 | -------------------------------------------------------------------------------- /src/components/ScrollButton/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ScrollButtonContainer = styled.div` 4 | display: flex; 5 | position: fixed; 6 | right: 4.5%; 7 | bottom: 13%; 8 | z-index: 999; 9 | 10 | svg { 11 | cursor: pointer; 12 | } 13 | 14 | div { 15 | display: flex; 16 | } 17 | 18 | div:nth-child(2) { 19 | transform: scaleY(-1); 20 | margin-left: 8px; 21 | } 22 | 23 | @media screen and (max-width: 700px) { 24 | bottom: 4.5%; 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /src/assets/DrawIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function DrawIcon() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default DrawIcon; 21 | -------------------------------------------------------------------------------- /src/assets/ImageIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function ImageIcon() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default ImageIcon; 21 | -------------------------------------------------------------------------------- /src/assets/X.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function X({ onClick }) { 4 | return ( 5 | 13 | 20 | 21 | ); 22 | } 23 | 24 | export default X; 25 | -------------------------------------------------------------------------------- /src/pages/Notice/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const NoticeBox = styled.div` 4 | display: flex; 5 | margin-bottom: 16px; 6 | align-items: center; 7 | margin-left: 26px; 8 | `; 9 | 10 | export const NoticeTitle = styled.p` 11 | font-size: 1rem; 12 | font-weight: 600; 13 | color: #007eff; 14 | cursor: pointer; 15 | line-height: 40px; 16 | margin-right: auto; 17 | `; 18 | 19 | export const NoticeDay = styled.p` 20 | font-size: 0.8rem; 21 | margin-right: 10px; 22 | color: #999999; 23 | `; 24 | -------------------------------------------------------------------------------- /src/components/Button/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Button = styled.button` 4 | width: ${props => props.width}px; 5 | height: ${props => props.height}px; 6 | background-color: ${props => props.backgroundColor}; 7 | border: none; 8 | font-size: 16px; 9 | color: ${props => props.color}; 10 | border-radius: ${props => props.borderRadius}px; 11 | font-weight: ${props => props.fontWeight}; 12 | float: ${props => props.float}; 13 | margin: ${props => props.margin}; 14 | outline: none; 15 | cursor: pointer; 16 | `; 17 | -------------------------------------------------------------------------------- /src/imgs/openToggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/imgs/closeToggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/InquiryItem/sytle.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const InquiryItemContainer = styled.div` 4 | width: 100%; 5 | display: flex; 6 | justify-content: center; 7 | margin-bottom: 30px; 8 | `; 9 | 10 | export const InquiryItemWrapper = styled.div` 11 | width: 98%; 12 | height: auto; 13 | display: flex; 14 | flex-direction: column; 15 | border-left: 4px solid #ddd; 16 | gap: 18px; 17 | padding: 10px 18px; 18 | line-height: 24px; 19 | cursor: pointer; 20 | 21 | &:hover { 22 | background-color: rgba(25, 25, 25, 0.04); 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /src/components/WriteBox/PreviewWriteBox/MarkdownConverter/index.jsx: -------------------------------------------------------------------------------- 1 | import useMarkdown from "../../../../Hooks/useMarkdown"; 2 | import DOMPurify from "dompurify"; 3 | 4 | function MarkDownConverter({ value }) { 5 | const { markdownToHtml } = useMarkdown(); 6 | 7 | const html = markdownToHtml(value); 8 | 9 | const cleanHtml = DOMPurify.sanitize(html); 10 | 11 | return ( 12 | <> 13 |
17 | 18 | ); 19 | } 20 | 21 | export default MarkDownConverter; 22 | -------------------------------------------------------------------------------- /src/components/HistoryDetailItem/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Content = styled.p` 4 | font-size: 1rem; 5 | color: #191919; 6 | line-height: 26px; 7 | margin-top: -40px; 8 | 9 | a { 10 | color: #007eff; 11 | } 12 | 13 | h1, 14 | h2, 15 | h3, 16 | h4 { 17 | margin-bottom: 20px; 18 | } 19 | `; 20 | 21 | export const NTBox = styled.div` 22 | margin-top: 10px; 23 | margin-left: 68%; 24 | `; 25 | 26 | export const Date = styled.p` 27 | text-align: right; 28 | font-size: 0.8rem; 29 | color: #999; 30 | font-weight: 400; 31 | `; 32 | -------------------------------------------------------------------------------- /src/pages/History/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import { useContent } from "../../Hooks"; 4 | import useHistory from "../../Hooks/useHistory"; 5 | import * as C from "../../components"; 6 | 7 | const History = () => { 8 | const { id } = useParams(); 9 | const { recordList } = useHistory({ id }); 10 | const { title } = useContent({ id }); 11 | 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default History; 20 | -------------------------------------------------------------------------------- /src/Hooks/index.js: -------------------------------------------------------------------------------- 1 | export { default as useFetch } from "./useFetch"; 2 | export { default as useLogin } from "./useLogin"; 3 | export { default as useFile } from "./useFile"; 4 | export { default as useUpload } from "./useUpload"; 5 | export { default as useInquiry } from "./useInquiry"; 6 | export { default as useNotice } from "./useNotice"; 7 | export { default as useMail } from "./useMail"; 8 | export { default as useSearchList } from "./useSearchList"; 9 | export { default as useContent } from "./useContent"; 10 | export { default as useEdit } from "./useEdit"; 11 | export { default as useDelete } from "./useDelete"; 12 | -------------------------------------------------------------------------------- /src/components/Graph/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./style"; 3 | 4 | function Graph({ 5 | titleChild, 6 | contentChild, 7 | backgroundColor, 8 | color, 9 | contentColor, 10 | }) { 11 | return ( 12 | <> 13 | 14 | 15 | {titleChild} 16 | 17 | 18 | {contentChild} 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default Graph; 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import reportWebVitals from "./reportWebVitals"; 5 | 6 | import { BrowserRouter } from "react-router-dom"; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById("root")); 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import GlobalStyle from "./styles/GlobalStyle"; 2 | import React from "react"; 3 | import Router from "./router"; 4 | import { ToastContainer } from "react-toastify"; 5 | import "react-toastify/dist/ReactToastify.css"; 6 | import { Provider } from "react-redux"; 7 | import { store } from "./store"; 8 | import { Analytics } from "@vercel/analytics/react"; 9 | 10 | function App() { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /src/assets/LinkIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function LinkIcon() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default LinkIcon; 21 | -------------------------------------------------------------------------------- /src/components/InquiryModal/index.jsx: -------------------------------------------------------------------------------- 1 | import * as S from "./style"; 2 | import * as I from "../../assets"; 3 | 4 | const InquiryModal = ({ setShowModal }) => { 5 | return ( 6 | <> 7 | setShowModal(false)} /> 8 | 9 | setShowModal(false)} /> 10 |

문의 완료

11 |

12 | 문의가 성공적으로 전송되었습니다. 승인 여부는 등록하신 메일로 전송 될 13 | 예정입니다. 14 |

15 | 16 |
17 | 18 | ); 19 | }; 20 | 21 | export default InquiryModal; 22 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from "@reduxjs/toolkit"; 2 | import uuidSlice from "./uuid"; 3 | import reissueSlice from "./reissue"; 4 | 5 | const NODE_ENV = process.env.NODE_ENV === "development"; 6 | 7 | const rootReducer = combineReducers({ 8 | uuid: uuidSlice.reducer, 9 | reissue: reissueSlice.reducer, 10 | }); 11 | 12 | const makeStore = () => { 13 | return configureStore({ 14 | reducer: rootReducer, 15 | devTools: NODE_ENV, 16 | middleware: getDefaultMiddleware => 17 | getDefaultMiddleware({ serializableCheck: false }).concat(), 18 | }); 19 | }; 20 | 21 | export const store = makeStore(); 22 | -------------------------------------------------------------------------------- /src/assets/BoldIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function BoldIcon() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default BoldIcon; 21 | -------------------------------------------------------------------------------- /src/components/BoardDetail/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Content = styled.p` 4 | font-size: 1rem; 5 | color: #191919; 6 | line-height: 26px; 7 | margin-top: -40px; 8 | 9 | a { 10 | color: #007eff; 11 | } 12 | 13 | h1, 14 | h2, 15 | h3, 16 | h4 { 17 | margin-bottom: 20px; 18 | } 19 | `; 20 | 21 | export const NTBox = styled.div` 22 | display: flex; 23 | flex-direction: column; 24 | gap: 6px; 25 | margin-top: 10px; 26 | margin-left: 68%; 27 | `; 28 | 29 | export const Date = styled.p` 30 | text-align: right; 31 | font-size: 0.8rem; 32 | color: #999; 33 | font-weight: 400; 34 | `; 35 | -------------------------------------------------------------------------------- /src/Hooks/useHistory.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { toast } from "react-toastify"; 3 | import API from "../apis"; 4 | 5 | const useHistory = ({ id }) => { 6 | const [recordList, setRecordList] = useState([]); 7 | 8 | useEffect(() => { 9 | const getRecordList = async () => { 10 | try { 11 | const { data } = await API.get(`/board/${id}/record`); 12 | 13 | setRecordList(data.boardRecordList); 14 | } catch (e) { 15 | toast.error("목록을 불러오는데 실패했습니다."); 16 | } 17 | }; 18 | 19 | getRecordList(); 20 | }, [id]); 21 | 22 | return { recordList }; 23 | }; 24 | 25 | export default useHistory; 26 | -------------------------------------------------------------------------------- /src/lib/Observable.js: -------------------------------------------------------------------------------- 1 | class Observable { 2 | constructor() { 3 | this.observers = []; 4 | } 5 | 6 | setObserver(callback) { 7 | const observer = new Observer(callback); 8 | this.observers.push(observer); 9 | 10 | return observer; 11 | } 12 | 13 | notifyAll() { 14 | this.observers.forEach(observer => { 15 | observer.callback(); 16 | }); 17 | } 18 | 19 | removeAll() { 20 | this.observers = []; 21 | } 22 | } 23 | 24 | class Observer { 25 | callback; 26 | 27 | constructor(callback) { 28 | this.callback = callback; 29 | } 30 | } 31 | 32 | const observable = new Observable(); 33 | 34 | export default observable; 35 | -------------------------------------------------------------------------------- /src/Hooks/useContent.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import useFetch from "./useFetch"; 3 | 4 | const useContent = ({ id }) => { 5 | const [state, setState] = useState({ 6 | id: "", 7 | content: "", 8 | title: "", 9 | createdDate: "", 10 | editedDate: "" 11 | }); 12 | 13 | const { fetch } = useFetch({ 14 | url: `/board/${id}`, 15 | method: "get", 16 | onSuccess: data => { 17 | setState(data); 18 | }, 19 | erros: { 20 | 400: "글을 불러오지 못했습니다." 21 | } 22 | }); 23 | 24 | useEffect(() => { 25 | fetch(); 26 | }, [id]); 27 | 28 | return state; 29 | }; 30 | 31 | export default useContent; 32 | -------------------------------------------------------------------------------- /src/components/HistoryDetailItem/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useMarkdown from "../../Hooks/useMarkdown"; 3 | import * as S from "./style"; 4 | import DOMPurify from "dompurify"; 5 | 6 | const HistoryDetailItem = ({ content, createdDate }) => { 7 | const { markdownToHtml } = useMarkdown(); 8 | 9 | const html = markdownToHtml(content); 10 | 11 | const cleanHtml = DOMPurify.sanitize(html); 12 | 13 | return ( 14 | <> 15 | 16 | 생성 일자 : {createdDate} 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default HistoryDetailItem; 25 | -------------------------------------------------------------------------------- /public/favicon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image/png", 26 | "density": "2.0" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Button/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./style"; 3 | 4 | function Button({ 5 | width, 6 | height, 7 | backgroundColor, 8 | children, 9 | borderRadius, 10 | color, 11 | fontWeight, 12 | float, 13 | margin, 14 | onClick 15 | }) { 16 | return ( 17 | 28 | {children} 29 | 30 | ); 31 | } 32 | 33 | export default Button; 34 | -------------------------------------------------------------------------------- /src/pages/NoticeWrite/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import * as C from "../../components"; 3 | import GetRole from "../../lib/GetRole"; 4 | import { useNavigate } from "react-router-dom"; 5 | import { toast } from "react-toastify"; 6 | 7 | const NoticeWrite = () => { 8 | const navigate = useNavigate(); 9 | const role = GetRole(); 10 | 11 | useEffect(() => { 12 | if (role !== "관리자") { 13 | toast.error("권한이 없습니다."); 14 | navigate("/"); 15 | } 16 | }, [role, navigate]); 17 | 18 | return ( 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default NoticeWrite; 26 | -------------------------------------------------------------------------------- /src/Hooks/useBoard.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { toast } from "react-toastify"; 3 | import API from "../apis"; 4 | 5 | const useBoard = ({ boardType }) => { 6 | const [boardList, setBoardList] = useState([]); 7 | 8 | useEffect(() => { 9 | const getBoardList = async () => { 10 | try { 11 | const { data } = await API.get(`/board`, { 12 | params: { boardType } 13 | }); 14 | 15 | setBoardList(data.boardList); 16 | } catch (e) { 17 | toast.error("목록을 불러오는데 실패했습니다."); 18 | } 19 | }; 20 | 21 | getBoardList(); 22 | }, [boardType]); 23 | 24 | return { boardList }; 25 | }; 26 | 27 | export default useBoard; 28 | -------------------------------------------------------------------------------- /src/components/BoardDetail/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./style"; 3 | import useMarkdown from "../../Hooks/useMarkdown"; 4 | import DOMPurify from "dompurify"; 5 | 6 | const BoardDetailItem = ({ content, createdDate, editedDate }) => { 7 | const { markdownToHtml } = useMarkdown(); 8 | 9 | const html = markdownToHtml(content); 10 | 11 | const cleanHtml = DOMPurify.sanitize(html); 12 | 13 | return ( 14 | <> 15 | 16 | 생성 일자 : {createdDate} 17 | 최근 수정 시각 : {editedDate} 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default BoardDetailItem; 26 | -------------------------------------------------------------------------------- /src/components/NoticeDetail/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./style"; 3 | import useMarkdown from "../../Hooks/useMarkdown"; 4 | import DOMPurify from "dompurify"; 5 | 6 | const NoticeDetailItem = ({ id, content, createdDate, editedDate }) => { 7 | const { markdownToHtml } = useMarkdown(); 8 | 9 | const html = markdownToHtml(content); 10 | 11 | const cleanHtml = DOMPurify.sanitize(html); 12 | 13 | return ( 14 | <> 15 | 16 | 작성일 : {createdDate} 17 | 수정일 : {editedDate} 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default NoticeDetailItem; 26 | -------------------------------------------------------------------------------- /src/components/NoticeDetail/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Content = styled.p` 4 | width: 42vw; 5 | margin-left: 20px; 6 | align-items: center; 7 | flex-direction: column; 8 | font-size: 1rem; 9 | color: #191919; 10 | line-height: 26px; 11 | 12 | a { 13 | color: #007eff; 14 | } 15 | 16 | h1, 17 | h2, 18 | h3, 19 | h4 { 20 | margin-bottom: 20px; 21 | } 22 | `; 23 | 24 | export const NTBox = styled.div` 25 | display: flex; 26 | flex-direction: column; 27 | gap: 6px; 28 | margin-top: 10px; 29 | margin-left: 68%; 30 | `; 31 | 32 | export const Date = styled.p` 33 | text-align: right; 34 | font-size: 0.8rem; 35 | color: #999; 36 | font-weight: 400; 37 | `; 38 | -------------------------------------------------------------------------------- /src/Hooks/useDelete.js: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { toast } from "react-toastify"; 4 | import API from "../apis"; 5 | 6 | const useDelete = ({ id }) => { 7 | const navigate = useNavigate(); 8 | 9 | const boardDelete = useCallback(async () => { 10 | try { 11 | await API.delete(`/board/${id}`); 12 | toast.success("삭제 성공"); 13 | navigate("/"); 14 | } catch (e) { 15 | if (e.response && e.response.status >= 403) { 16 | toast.error("자신의 글만 삭제할 수 있습니다."); 17 | } else { 18 | toast.error("글 삭제에 실패했습니다."); 19 | } 20 | } 21 | }, [navigate, id]); 22 | 23 | return { boardDelete }; 24 | }; 25 | 26 | export default useDelete; 27 | -------------------------------------------------------------------------------- /src/components/Header/dropMenu/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const DropContainer = styled.div` 4 | width: 100%; 5 | background-color: #3e9dff; 6 | height: auto; 7 | color: #fff; 8 | padding-left: 16vw; 9 | padding-bottom: 2vw; 10 | position: absolute; 11 | z-index: 2; 12 | `; 13 | export const DropMenu = styled.div` 14 | display: flex; 15 | gap: 3.7vw; 16 | `; 17 | 18 | export const DropItem = styled.div` 19 | margin-top: 1.2vw; 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | gap: 1.2vw; 24 | span { 25 | font-weight: 700; 26 | cursor: pointer; 27 | font-size: 16px; 28 | } 29 | a { 30 | color: #fff; 31 | } 32 | `; 33 | -------------------------------------------------------------------------------- /src/components/ScrollButton/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./style"; 3 | import * as I from "../../assets"; 4 | 5 | export default function ScrollButton() { 6 | function scrollToBottom() { 7 | window.scroll({ 8 | top: document.documentElement.scrollHeight, 9 | behavior: "smooth", 10 | }); 11 | } 12 | 13 | function scrollToTop() { 14 | window.scrollTo({ 15 | top: 0, 16 | behavior: "smooth", 17 | }); 18 | } 19 | return ( 20 | 21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/Hooks/useSearchList.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import API from "../apis"; 3 | 4 | const useSearchList = ({ title }) => { 5 | const [searchList, setSearchList] = useState([]); 6 | 7 | useEffect(() => { 8 | const fetchData = async () => { 9 | try { 10 | if (typeof title === "string" && title.trim() !== "") { 11 | const { data } = await API.get(`/board/search?title=${title}`, { 12 | params: { title } 13 | }); 14 | 15 | const boardTitleList = data.boardTitleList; 16 | setSearchList(boardTitleList); 17 | } 18 | } catch (error) {} 19 | }; 20 | 21 | fetchData(); 22 | }, [title]); 23 | 24 | return { searchList }; 25 | }; 26 | 27 | export default useSearchList; 28 | -------------------------------------------------------------------------------- /src/assets/Arrow.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Arrow() { 4 | return ( 5 |
6 | 13 | 19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/Notice.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Notice() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default Notice; 21 | -------------------------------------------------------------------------------- /src/assets/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Search() { 4 | return ( 5 | 12 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/EditNotice/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as C from "../../components"; 3 | import { useNotice } from "../../Hooks"; 4 | import { useNavigate, useParams } from "react-router-dom"; 5 | import GetRole from "../../lib/GetRole"; 6 | import { toast } from "react-toastify"; 7 | import { useEffect } from "react"; 8 | 9 | const EditNotice = () => { 10 | const navigate = useNavigate(); 11 | const role = GetRole(); 12 | 13 | useEffect(() => { 14 | if (role !== "관리자") { 15 | toast.error("권한이 없습니다."); 16 | navigate("/"); 17 | } 18 | }, [role, navigate]); 19 | const { id } = useParams(); 20 | const { state } = useNotice({ props: { id } }); 21 | 22 | return ( 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default EditNotice; 30 | -------------------------------------------------------------------------------- /src/pages/Inquiry/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledTitle = styled.strong` 4 | font-size: 18px; 5 | max-width: 80%; 6 | color: #000; 7 | `; 8 | 9 | export const StyledContents = styled.p` 10 | color: #636363; 11 | font-size: 14px; 12 | `; 13 | 14 | export const Sort = styled.div` 15 | width: max-content; 16 | height: 20px; 17 | border: 1px solid #dddddd; 18 | color: #999999; 19 | font-size: 12px; 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | padding: 7px 4px; 24 | margin-right: 10px; 25 | `; 26 | 27 | export const Date = styled.p` 28 | font-size: 0.8rem; 29 | color: #999999; 30 | `; 31 | 32 | export const InquiryTitleContainer = styled.div` 33 | display: flex; 34 | flex-direction: column; 35 | gap: 12px; 36 | margin-left: 8px; 37 | `; 38 | 39 | export const StyledTitleContainer = styled.div` 40 | display: flex; 41 | gap: 8px; 42 | `; 43 | -------------------------------------------------------------------------------- /src/assets/GAuthLogo.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function GAuthLogo() { 4 | return ( 5 | 12 | 18 | 19 | ); 20 | } 21 | 22 | export default GAuthLogo; 23 | -------------------------------------------------------------------------------- /src/assets/Etc.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Etc() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default Etc; 21 | -------------------------------------------------------------------------------- /src/components/Logout/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./style"; 3 | import * as I from "../../assets"; 4 | 5 | function Logout({ setShowLogout, onConfirm }) { 6 | function showLogoutModal() { 7 | setShowLogout(prev => !prev); 8 | } 9 | const onClick = () => { 10 | setShowLogout(prev => !prev); 11 | onConfirm(); 12 | }; 13 | return ( 14 | <> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 로그아웃 하시겠습니까? 22 | 23 | 24 | 아니요 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | export default Logout; 34 | -------------------------------------------------------------------------------- /src/components/Detail/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import * as I from "../../assets"; 3 | import * as S from "./style"; 4 | 5 | export default function Detail({ 6 | title, 7 | hasNumber, 8 | children, 9 | number, 10 | detailContent 11 | }) { 12 | const [detailActive, setDetailActive] = useState(true); 13 | return ( 14 | 15 | 16 | setDetailActive(prev => !prev)}> 17 | 18 | 19 | {hasNumber && ( 20 | {number}. 21 | )} 22 | {title} 23 | 24 | 25 | {detailActive && ( 26 | 27 | {children} 28 | 29 | )} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/Hooks/useMail.js: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import API from "../apis"; 3 | import { toast } from "react-toastify"; 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | const useMail = ({ props }) => { 7 | const navigate = useNavigate(); 8 | 9 | const postApproveMail = useCallback(async () => { 10 | try { 11 | await API.post(`/inquiry/approve/${props.id}`); 12 | toast.success("승인 메일 발송 성공"); 13 | 14 | navigate("/inquiry"); 15 | } catch (e) { 16 | toast.error("메일 발송 실패"); 17 | } 18 | }, [props.id, navigate]); 19 | 20 | const postRefusalMail = useCallback(async () => { 21 | try { 22 | await API.post(`/inquiry/refusal/${props.id}`, { 23 | comment: props.refusalReason 24 | }); 25 | toast.success("거부 메일 발송 성공"); 26 | navigate("/inquiry"); 27 | } catch (e) { 28 | toast.error("메일 발송 실패"); 29 | } 30 | }, [props, navigate]); 31 | 32 | return { postApproveMail, postRefusalMail }; 33 | }; 34 | 35 | export default useMail; 36 | -------------------------------------------------------------------------------- /src/pages/Role/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const RoleContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | align-items: flex-end; 7 | `; 8 | 9 | export const InputContainer = styled.div` 10 | width: 100%; 11 | height: 48px; 12 | border: solid 1px #c0c0c0; 13 | margin: 50px 0 10px 0; 14 | display: flex; 15 | align-items: center; 16 | `; 17 | 18 | export const RoleTitle = styled.div` 19 | font-weight: 700; 20 | color: #999; 21 | margin-left: 16px; 22 | `; 23 | 24 | export const RoleInput = styled.input` 25 | width: 20%; 26 | height: 60%; 27 | border: solid 1px #ddd; 28 | padding: 0 6px; 29 | color: #999; 30 | margin-left: 16px; 31 | font-size: 1rem; 32 | &:focus { 33 | outline: none; 34 | } 35 | `; 36 | 37 | export const Select = styled.select` 38 | width: 20%; 39 | height: 60%; 40 | border: solid 1px #ddd; 41 | margin-left: 16px; 42 | outline: 0; 43 | color: #999; 44 | cursor: pointer; 45 | `; 46 | export const Img = styled.img` 47 | width: 40vw; 48 | `; 49 | -------------------------------------------------------------------------------- /src/pages/Major/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useBoard from "../../Hooks/useBoard"; 3 | import * as C from "../../components"; 4 | import * as S from "./style"; 5 | 6 | export default function Student() { 7 | const { boardList } = useBoard({ boardType: "MAJOR" }); 8 | 9 | if (!boardList) return; 10 | 11 | const handleBoardItemClick = boardId => { 12 | const boardUrl = `/board/${boardId}`; 13 | window.location.href = boardUrl; 14 | }; 15 | 16 | return ( 17 | 24 | 25 | {boardList.map(item => ( 26 | 27 | 28 | handleBoardItemClick(item.id)}> 29 | {item.title} 30 | 31 | 32 | 33 | ))} 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/Hooks/useFile.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import API from "../apis"; 3 | import { toast } from "react-toastify"; 4 | 5 | const useFile = () => { 6 | const [isLoading, setIsLoading] = useState(false); 7 | 8 | const upload = useCallback(async file => { 9 | const formData = new FormData(); 10 | setIsLoading(true); 11 | 12 | file.forEach(f => { 13 | formData.append("file", f); 14 | }); 15 | 16 | try { 17 | const { data } = await API.post("/file", formData, { 18 | header: { "Content-Type": "multipart/form-data" } 19 | }); 20 | 21 | setIsLoading(false); 22 | 23 | return data.awsUrl; 24 | } catch (e) { 25 | if (e.response && e.response.status >= 500) { 26 | toast.error("서버에 문제가 생겼습니다."); 27 | } else if (e.response && e.response.status >= 400) { 28 | toast.error("허용되지 않는 파일 형식입니다."); 29 | } else { 30 | toast.error("이미지 업로드에 실패했습니다."); 31 | } 32 | setIsLoading(false); 33 | } 34 | }, []); 35 | 36 | return { upload, isLoading }; 37 | }; 38 | 39 | export default useFile; 40 | -------------------------------------------------------------------------------- /src/components/PromotionPage/index.jsx: -------------------------------------------------------------------------------- 1 | import * as S from "./style"; 2 | import * as I from "../../assets"; 3 | import PromotionCom from "../../imgs/PromotionCom.webp"; 4 | import { gauthLoginUri } from "../../lib/GAuthLoginUrl"; 5 | 6 | function PromotionPage() { 7 | return ( 8 | 9 | 10 | 11 | 12 |

GSM 학생들이 키워나가는 지식의 나무

13 | 14 |
15 |

16 | G무위키는 앰퍼샌드 팀에서 개발한 GSM만의 나무위키 프로젝트 입니다.{" "} 17 |
18 | GSM에 관한 여러 정보를 공유해보세요! 19 |

20 | window.location.replace(gauthLoginUri)} 22 | > 23 | Sign in with GAuth 24 | 25 |
26 | 27 |
28 |
29 | ); 30 | } 31 | 32 | export default PromotionPage; 33 | -------------------------------------------------------------------------------- /src/Hooks/useInquiry.js: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { toast } from "react-toastify"; 4 | import API from "../apis"; 5 | import GetRole from "../lib/GetRole"; 6 | 7 | const useInquiry = ({ props }) => { 8 | const navigate = useNavigate(); 9 | 10 | const data = GetRole(); 11 | 12 | const inquiryUpload = useCallback(async () => { 13 | try { 14 | await API.post(`/inquiry`, { 15 | title: props.title, 16 | content: props.content, 17 | inquiryType: props.category 18 | }); 19 | toast.success("문의 등록 성공"); 20 | 21 | if (data === "관리자") { 22 | navigate("/inquiry"); 23 | } else if (data === "사용자") { 24 | navigate("/"); 25 | } 26 | } catch (e) { 27 | if (e.response && e.response.status >= 403) { 28 | toast.error("권한이 없습니다."); 29 | } else if (e.response && e.response.status >= 401) { 30 | toast.error("로그인이 필요합니다."); 31 | } else { 32 | toast.error("문의 글을 작성할 수 없습니다."); 33 | } 34 | } 35 | }, [props, navigate, data]); 36 | 37 | return { inquiryUpload }; 38 | }; 39 | 40 | export default useInquiry; 41 | -------------------------------------------------------------------------------- /src/Hooks/useUpload.js: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | import { useCallback } from "react"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { toast } from "react-toastify"; 5 | import API from "../apis"; 6 | 7 | const useUpload = ({ props }) => { 8 | const navigate = useNavigate(); 9 | 10 | const uploadHandler = useCallback(async () => { 11 | try { 12 | await API.post(`/board`, { 13 | title: props.title, 14 | content: props.content, 15 | boardType: props.category, 16 | boardDetailType: props.detailCategory 17 | }); 18 | 19 | toast.success("글 등록에 성공하였습니다."); 20 | navigate("/"); 21 | } catch (e) { 22 | if (!(e instanceof AxiosError)) { 23 | toast.error("편집에 실패하였습니다."); 24 | return; 25 | } 26 | 27 | if (e.response && e.response.status >= 500) { 28 | toast.error("글 등록에 실패하였습니다."); 29 | } else if (e.response && e.response.status >= 409) { 30 | toast.error("이미 해당하는 글 제목이 있습니다."); 31 | } else { 32 | toast.error("글 등록에 실패하였습니다."); 33 | } 34 | } 35 | }, [props, navigate]); 36 | 37 | return { uploadHandler }; 38 | }; 39 | 40 | export default useUpload; 41 | -------------------------------------------------------------------------------- /src/Hooks/useLogin.js: -------------------------------------------------------------------------------- 1 | import TokenManager from "../apis/TokenManager"; 2 | import { useEffect } from "react"; 3 | import useFetch from "./useFetch"; 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | const useLogin = () => { 7 | const navigate = useNavigate(); 8 | const searchParams = new URLSearchParams(window.location.search); 9 | const gauthCode = searchParams.get("code"); 10 | 11 | const { fetch } = useFetch({ 12 | url: "/auth", 13 | method: "post", 14 | skipLogin: true, 15 | onSuccess: data => { 16 | if (typeof window !== "undefined") { 17 | const tokenManager = new TokenManager(); 18 | tokenManager.setTokens(data); 19 | } 20 | navigate("/"); 21 | window.location.reload(); 22 | }, 23 | onFailure: () => { 24 | navigate("/promotion"); 25 | } 26 | }); 27 | 28 | useEffect(() => { 29 | const checkLoggedIn = () => { 30 | const tokenManager = new TokenManager(); 31 | return tokenManager.initToken(); 32 | }; 33 | 34 | if (checkLoggedIn()) { 35 | navigate("/"); 36 | return; 37 | } 38 | 39 | if (!gauthCode) return; 40 | fetch({ code: gauthCode }); 41 | }, [gauthCode, navigate]); 42 | }; 43 | 44 | export default useLogin; 45 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | export { default as Main } from "./Main"; 2 | export { default as NotFound } from "./NotFound"; 3 | export { default as Club } from "./Club"; 4 | export { default as Event } from "./Event"; 5 | export { default as Major } from "./Major"; 6 | export { default as Notice } from "./Notice"; 7 | export { default as NoticeWrite } from "./NoticeWrite"; 8 | export { default as Teacher } from "./Teacher"; 9 | export { default as Student } from "./Student"; 10 | export { default as BoardDetail } from "./BoardDetail"; 11 | export { default as Post } from "./Post"; 12 | export { default as Inquiry } from "./Inquiry"; 13 | export { default as InquiryDetail } from "./InquiryDetail"; 14 | export { default as InquiryWrite } from "./InquiryWrite"; 15 | export { default as Schedule } from "./Schedule"; 16 | export { default as History } from "./History"; 17 | export { default as HistoryDetail } from "./HistoryDetail"; 18 | export { default as Role } from "./Role"; 19 | export { default as NoticeDetail } from "./NoticeDetail"; 20 | export { default as Edit } from "./Edit"; 21 | export { default as EditNotice } from "./EditNotice"; 22 | export { default as Promotion } from "./Promotion"; 23 | export { default as Project } from "./Project"; 24 | -------------------------------------------------------------------------------- /src/Hooks/useMarkdown.js: -------------------------------------------------------------------------------- 1 | import { marked } from 'marked'; 2 | 3 | function useMarkdown() { 4 | 5 | const initialState = href => (href.includes('gmuwiki') ? false : true); 6 | 7 | const markdownToHtml = (value) => { 8 | const renderer = new marked.Renderer(); 9 | 10 | // 코드블럭, 인용문, 링크 스타일링을 위한 코드이다. 11 | renderer.code = code => { 12 | return `
${code}
`; 13 | }; 14 | 15 | renderer.blockquote = quote => { 16 | return `
${quote}
`; 17 | }; 18 | 19 | renderer.link = (href, title, text) => { 20 | const target = initialState(href) 21 | return `${text}`; 22 | }; 23 | 24 | const options = { 25 | renderer, 26 | // marked 경고 메시지를 해제하는 옵션이다. 27 | mangle: false, 28 | headerIds: false 29 | }; 30 | 31 | const convertedValue = value 32 | .replace(/>>/g, `
`) 33 | .replace(/>==/g, ``) 34 | .replace(/==`) 35 | .replace(/<`); 36 | 37 | const html = marked(convertedValue, options); 38 | 39 | return html; 40 | } 41 | 42 | return { markdownToHtml }; 43 | } 44 | 45 | export default useMarkdown; -------------------------------------------------------------------------------- /src/apis/index.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import EnvConfig from "./EnvConfig"; 3 | import TokenManager from "./TokenManager"; 4 | import { reissueToken } from "../store/reissue"; 5 | import { store } from "../store"; 6 | 7 | const API = axios.create({ 8 | baseURL: EnvConfig.GMUWIKI_SERVER_URL, 9 | withCredentials: true 10 | }); 11 | 12 | API.interceptors.request.use(async config => { 13 | const tokenManager = new TokenManager(); 14 | 15 | if ( 16 | !tokenManager.validateToken( 17 | tokenManager.accessExp, 18 | tokenManager.accessToken 19 | ) && 20 | tokenManager.validateToken( 21 | tokenManager.refreshExp, 22 | tokenManager.refreshToken 23 | ) 24 | ) { 25 | await store.dispatch(reissueToken()); 26 | tokenManager.initToken(); 27 | } else if ( 28 | !tokenManager.validateToken( 29 | tokenManager.accessExp, 30 | tokenManager.accessToken 31 | ) && 32 | !tokenManager.validateToken( 33 | tokenManager.refreshExp, 34 | tokenManager.refreshToken 35 | ) 36 | ) 37 | tokenManager.removeTokens(); 38 | 39 | config.headers["Authorization"] = tokenManager.accessToken 40 | ? `Bearer ${tokenManager.accessToken}` 41 | : undefined; 42 | 43 | return config; 44 | }); 45 | 46 | export default API; 47 | -------------------------------------------------------------------------------- /src/pages/HistoryDetail/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import { useFetch } from "../../Hooks"; 4 | import * as C from "../../components"; 5 | 6 | const HistoryDetail = () => { 7 | const [state, setState] = useState({ 8 | id: "", 9 | content: "", 10 | title: "", 11 | createdDate: "", 12 | editedDate: "" 13 | }); 14 | 15 | let { id } = useParams(); 16 | 17 | const { fetch } = useFetch({ 18 | url: `/board/${id}/record/detail`, 19 | method: "get", 20 | onSuccess: data => { 21 | setState(data); 22 | }, 23 | erros: { 24 | 400: "글을 불러오지 못했습니다." 25 | } 26 | }); 27 | 28 | useEffect(() => { 29 | fetch(); 30 | }, []); 31 | 32 | const formattedCreatedDate = new Date(state.createdDate).toLocaleString(); 33 | const formattedEditedDate = new Date(state.editedDate).toLocaleString(); 34 | 35 | return ( 36 | 37 | 38 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default HistoryDetail; 49 | -------------------------------------------------------------------------------- /src/components/Refusal/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import * as S from "./style"; 3 | import * as I from "../../assets"; 4 | import * as C from "../../components"; 5 | import { useMail } from "../../Hooks"; 6 | 7 | function Refusal({ showLogout, setShowLogout, id }) { 8 | function showLogoutModal() { 9 | setShowLogout(prev => !prev); 10 | } 11 | 12 | const [refusalReason, setRefusalReason] = useState(""); 13 | 14 | const changeRefusal = e => { 15 | setRefusalReason(e.target.value); 16 | }; 17 | 18 | const { postRefusalMail } = useMail({ props: { refusalReason, id } }); 19 | 20 | const onClick = () => { 21 | setShowLogout(prev => !prev); 22 | const shouldRefusal = window.confirm("정말로 거부하시겠습니까?"); 23 | 24 | if (shouldRefusal) { 25 | postRefusalMail(); 26 | } 27 | }; 28 | return ( 29 | <> 30 | 31 | 32 | 33 | 34 | 거부 사유 입력 35 | 36 | 메일 전송 37 | 38 | 39 | 40 | ); 41 | } 42 | 43 | export default Refusal; 44 | -------------------------------------------------------------------------------- /src/components/PageContainer/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const PageContainer = styled.div` 4 | position: relative; 5 | width: 70vw; 6 | min-height: calc(100vh - 150px); 7 | height: auto; 8 | background-color: #fff; 9 | margin: 0 4vw 20px 8vw; 10 | padding: 0 3vw 5vw 3vw; 11 | border-top: none; 12 | border: 1px solid #c0c0c0; 13 | @media screen and (max-width: 1000px) { 14 | width: 100%; 15 | margin: 0; 16 | min-height: calc(100vh - 130px); 17 | } 18 | `; 19 | 20 | export const Page = styled.div` 21 | display: flex; 22 | `; 23 | 24 | export const TitleContainer = styled.div` 25 | color: #636363; 26 | font-size: 2.4rem; 27 | font-weight: 600; 28 | margin: 3vw 0 2vw 0; 29 | display: flex; 30 | justify-content: space-between; 31 | 32 | @media screen and (max-width: 500px) { 33 | font-size: 2rem; 34 | } 35 | `; 36 | 37 | export const SubTitleContainer = styled.div` 38 | border: 1px solid #d9d9d9; 39 | height: 5vh; 40 | line-height: 5vh; 41 | padding: 0 1vw; 42 | color: #191919; 43 | margin-bottom: 32px; 44 | 45 | span:nth-child(2) { 46 | color: #007eff; 47 | } 48 | `; 49 | 50 | export const ContentsButtonContainer = styled.div` 51 | display: flex; 52 | gap: 1px; 53 | 54 | button { 55 | border: 2px solid #dddddd; 56 | } 57 | `; 58 | -------------------------------------------------------------------------------- /src/components/Detail/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const DetailContainer = styled.div` 4 | svg { 5 | transform: ${props => !props.detailActive && "rotate(0.75turn)"}; 6 | cursor: pointer; 7 | } 8 | color: ${props => !props.detailActive && "rgba(0, 0, 0, 0.5)"}; 9 | margin: 38px 0 38px 0; 10 | `; 11 | 12 | export const DetailNumber = styled.span` 13 | font-size: 24px; 14 | color: #007eff; 15 | opacity: ${props => (!props.detailActive ? 0.5 : 1)}; 16 | font-weight: 600; 17 | margin-right: 8px; 18 | `; 19 | 20 | export const DetailTitle = styled.span` 21 | font-size: 25px; 22 | font-weight: 600; 23 | margin-bottom: 10px; 24 | `; 25 | 26 | export const DetailContent = styled.div` 27 | margin-top: 28px; 28 | margin-left: 8px; 29 | line-height: 24px; 30 | display: flex; 31 | align-items: ${props => (props.detailContent ? "center" : "initial")}; 32 | flex-direction: column; 33 | `; 34 | 35 | export const DetailTitleContainer = styled.div` 36 | display: flex; 37 | `; 38 | 39 | export const ArrowContainer = styled.div` 40 | margin-right: 8px; 41 | `; 42 | 43 | export const DetailBorder = styled.div` 44 | border-bottom: 1px solid #d9d9d9; 45 | `; 46 | 47 | export const DetailLink = styled.span` 48 | color: #007eff; 49 | font-weight: 600; 50 | `; 51 | 52 | export const LinkContainer = styled.div` 53 | margin-left: 28px; 54 | `; 55 | -------------------------------------------------------------------------------- /src/assets/index.jsx: -------------------------------------------------------------------------------- 1 | export { default as X } from "./X"; 2 | export { default as LoginLogo } from "./LoginLogo"; 3 | export { default as GAuthLogo } from "./GAuthLogo"; 4 | export { default as School } from "./School"; 5 | export { default as Etc } from "./Etc"; 6 | export { default as Logo } from "./Logo"; 7 | export { default as Notice } from "./Notice"; 8 | export { default as NotFound } from "./NotFound"; 9 | export { default as Arrow } from "./Arrow"; 10 | export { default as ScrollButton } from "./ScrollButton"; 11 | export { default as H1Icon } from "./H1Icon"; 12 | export { default as H2Icon } from "./H2Icon"; 13 | export { default as H3Icon } from "./H3Icon"; 14 | export { default as H4Icon } from "./H4Icon"; 15 | export { default as DivideLineIcon } from "./DivideLineIcon"; 16 | export { default as BoldIcon } from "./BoldIcon"; 17 | export { default as InclineIcon } from "./InclineIcon"; 18 | export { default as DrawIcon } from "./DrawIcon"; 19 | export { default as QuoteIcon } from "./QuoteIcon"; 20 | export { default as LinkIcon } from "./LinkIcon"; 21 | export { default as CodeIcon } from "./CodeIcon"; 22 | export { default as Toggle } from "./Toggle"; 23 | export { default as Logout } from "./Logout"; 24 | export { default as ImageIcon } from "./ImageIcon"; 25 | export { default as Search } from "./Search"; 26 | export { default as Cloud } from "./Cloud"; 27 | export { default as AlarmIcon } from "./AlarmIcon"; 28 | -------------------------------------------------------------------------------- /src/pages/BoardDetail/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as C from "../../components"; 3 | import { useContent, useDelete } from "../../Hooks"; 4 | import { useParams } from "react-router-dom"; 5 | 6 | const BoardDetail = () => { 7 | let { id } = useParams(); 8 | 9 | const state = useContent({ id }); 10 | const { boardDelete } = useDelete({ id }); 11 | 12 | const handleDelete = () => { 13 | const shouldDelete = window.confirm("정말로 삭제하시겠습니까?"); 14 | 15 | if (shouldDelete) { 16 | boardDelete(); 17 | } 18 | }; 19 | 20 | const formattedCreatedDate = new Date(state.createdDate).toLocaleString(); 21 | const formattedEditedDate = new Date(state.editedDate).toLocaleString(); 22 | 23 | return ( 24 | <> 25 | 34 | 35 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default BoardDetail; 49 | -------------------------------------------------------------------------------- /src/components/InquiryModal/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ModalOverlay = styled.div` 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | width: 100vw; 8 | height: 100vh; 9 | background-color: rgba(0, 0, 0, 0.3); 10 | z-index: 100; 11 | position: fixed; 12 | `; 13 | 14 | export const InquiryModalContainer = styled.div` 15 | width: 20vw; 16 | height: 33vh; 17 | z-index: 101; 18 | background: #ffffff; 19 | border: none; 20 | border-radius: 3px; 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | justify-content: space-between; 25 | position: fixed; 26 | top: 50%; 27 | left: 50%; 28 | transform: translate(-50%, -50%); 29 | padding: 38px 18px 26px 18px; 30 | 31 | & > svg { 32 | position: absolute; 33 | right: 16px; 34 | top: 16px; 35 | cursor: pointer; 36 | } 37 | 38 | h2 { 39 | color: #000000; 40 | font-size: 1.485rem; 41 | } 42 | 43 | p { 44 | color: #999999; 45 | font-weight: 300; 46 | width: 12vw; 47 | text-align: center; 48 | font-size: 1rem; 49 | line-height: 22px; 50 | } 51 | 52 | button { 53 | width: 100%; 54 | height: 36px; 55 | border: none; 56 | outline: none; 57 | border-radius: 4px; 58 | background: #007eff; 59 | color: #ffffff; 60 | font-size: 0.912rem; 61 | font-weight: 400; 62 | cursor: pointer; 63 | } 64 | ` -------------------------------------------------------------------------------- /src/assets/ScrollButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function ScrollButton() { 4 | return ( 5 | 12 | 20 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/assets/LoginLogo.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function LoginLogo() { 4 | return ( 5 | 12 | 19 | 20 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | export default LoginLogo; 40 | -------------------------------------------------------------------------------- /src/components/InquiryDetailItem/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Content = styled.p` 4 | width: 51vw; 5 | min-height: 30vh; 6 | align-items: center; 7 | flex-direction: column; 8 | font-size: 1rem; 9 | color: #191919; 10 | line-height: 26px; 11 | 12 | a { 13 | color: #007eff; 14 | } 15 | 16 | h1, 17 | h2, 18 | h3, 19 | h4 { 20 | margin-bottom: 20px; 21 | } 22 | `; 23 | 24 | export const NTBox = styled.div` 25 | width: 100%; 26 | display: flex; 27 | flex-direction: column; 28 | align-items: flex-end; 29 | color: #999; 30 | margin-right: 3vw; 31 | margin-top: 8px; 32 | `; 33 | 34 | export const Name = styled.p` 35 | font-size: 0.8rem; 36 | `; 37 | 38 | export const CreatedDate = styled.p` 39 | font-size: 0.8rem; 40 | font-weight: 400; 41 | `; 42 | 43 | export const BtnBox = styled.div` 44 | position: absolute; 45 | display: flex; 46 | width: 100%; 47 | justify-content: flex-end; 48 | right: 40px; 49 | bottom: 40px; 50 | `; 51 | 52 | export const ApproveBtn = styled.button` 53 | width: 80px; 54 | height: 35px; 55 | background-color: #007eff; 56 | border: none; 57 | color: #fff; 58 | cursor: pointer; 59 | `; 60 | 61 | export const RefusalBtn = styled.button` 62 | width: 80px; 63 | height: 35px; 64 | color: #007eff; 65 | border: 1px solid #007eff; 66 | background: none; 67 | margin-left: 12px; 68 | cursor: pointer; 69 | `; 70 | -------------------------------------------------------------------------------- /src/pages/NoticeDetail/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as C from "../../components"; 3 | import { useNotice } from "../../Hooks"; 4 | import { useParams } from "react-router-dom"; 5 | import GetRole from "../../lib/GetRole"; 6 | 7 | const NoticeDetail = () => { 8 | const data = GetRole(); 9 | 10 | let { id } = useParams(); 11 | 12 | const { deleteNotice, state } = useNotice({ props: { id } }); 13 | 14 | const handleDelete = () => { 15 | const shouldDelete = window.confirm("정말로 삭제하시겠습니까?"); 16 | 17 | if (shouldDelete) { 18 | deleteNotice(); 19 | } 20 | }; 21 | 22 | const hasDeleteButton = data === "관리자"; 23 | const hasEditButton = data === "관리자"; 24 | 25 | const formattedCreatedDate = new Date(state.createdDate).toLocaleString(); 26 | const formattedEditedDate = new Date(state.editedDate).toLocaleString(); 27 | 28 | return ( 29 | 37 | 38 | 45 | 46 | 47 | ); 48 | }; 49 | 50 | export default NoticeDetail; 51 | -------------------------------------------------------------------------------- /src/assets/AlarmIcon.jsx: -------------------------------------------------------------------------------- 1 | function AlarmIcon() { 2 | return ( 3 | 10 | 14 | 15 | ); 16 | } 17 | 18 | export default AlarmIcon; 19 | -------------------------------------------------------------------------------- /src/assets/H1Icon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function H1Icon() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default H1Icon; 21 | -------------------------------------------------------------------------------- /src/pages/Club/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useBoard from "../../Hooks/useBoard"; 3 | import * as C from "../../components"; 4 | import * as S from "./style"; 5 | 6 | export default function Student() { 7 | const { boardList } = useBoard({ boardType: "CLUB" }); 8 | 9 | if (!boardList) return null; 10 | 11 | const clubTypes = [ 12 | { title: "전공 동아리", type: "MAJOR" }, 13 | { title: "자율 동아리", type: "CA" } 14 | ]; 15 | 16 | const renderBoardItems = clubType => { 17 | const filteredItems = boardList 18 | .filter(item => item.boardDetailType === clubType) 19 | .sort((a, b) => a.title.localeCompare(b.title, "ko")); 20 | 21 | const handleBoardItemClick = boardId => { 22 | const boardUrl = `/board/${boardId}`; 23 | window.location.href = boardUrl; 24 | }; 25 | 26 | return filteredItems.map(item => ( 27 | 28 | 29 | handleBoardItemClick(item.id)}> 30 | {item.title} 31 | 32 | 33 | 34 | )); 35 | }; 36 | 37 | return ( 38 | 45 | {clubTypes.map(clubType => ( 46 | 47 | {renderBoardItems(clubType.type)} 48 | 49 | ))} 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/components/HistoryItem/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./style"; 3 | import { Link } from "react-router-dom"; 4 | 5 | const HistoryItem = ({ recordList }) => { 6 | const sortedRecordList = [...recordList].sort((a, b) => { 7 | const dateA = new Date(a.createdDate); 8 | const dateB = new Date(b.createdDate); 9 | return dateB - dateA; 10 | }); 11 | 12 | return ( 13 | <> 14 | {sortedRecordList.map(item => { 15 | const createdDate = new Date(item.createdDate); 16 | const formattedDate = `${createdDate.getFullYear()}-${( 17 | createdDate.getMonth() + 1 18 | ) 19 | .toString() 20 | .padStart(2, "0")}-${createdDate 21 | .getDate() 22 | .toString() 23 | .padStart(2, "0")} [${createdDate 24 | .getHours() 25 | .toString() 26 | .padStart(2, "0")}:${createdDate 27 | .getMinutes() 28 | .toString() 29 | .padStart(2, "0")}:${createdDate 30 | .getSeconds() 31 | .toString() 32 | .padStart(2, "0")}]`; 33 | 34 | return ( 35 | 36 | 37 | 38 | {formattedDate} 39 | 수정자: {item.name} 40 | 41 | 42 | 43 | ); 44 | })} 45 | 46 | ); 47 | }; 48 | 49 | export default HistoryItem; 50 | -------------------------------------------------------------------------------- /src/pages/Notice/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { useFetch } from "../../Hooks"; 4 | import * as C from "../../components"; 5 | import GetRole from "../../lib/GetRole"; 6 | import * as S from "./style"; 7 | 8 | export default function Notice() { 9 | const data = GetRole(); 10 | 11 | const [noticeList, setNoticeList] = useState([]); 12 | 13 | const { fetch } = useFetch({ 14 | url: `/notice`, 15 | method: "get", 16 | onSuccess: data => { 17 | const sortedList = data.noticeList.sort((a, b) => 18 | b.createdDate.localeCompare(a.createdDate) 19 | ); 20 | setNoticeList(sortedList); 21 | }, 22 | errors: { 23 | 400: "공지 목록을 불러오지 못함." 24 | } 25 | }); 26 | 27 | useEffect(() => { 28 | fetch(); 29 | }, []); 30 | 31 | return ( 32 | 41 | 42 | {noticeList.map(item => ( 43 | 44 | 45 | 46 | {item.title} 47 | {item.createdDate.substring(0, 10)} 48 | 49 | 50 | 51 | ))} 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/Hooks/useFetch.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import API from "../apis"; 3 | import { AxiosError } from "axios"; 4 | import { toast } from "react-toastify"; 5 | 6 | const useFetch = options => { 7 | const [data, setData] = useState(null); 8 | const [isLoading, setIsLoading] = useState(false); 9 | 10 | const fetch = useCallback( 11 | async body => { 12 | setIsLoading(true); 13 | try { 14 | const { data } = await API({ 15 | url: options.url, 16 | method: options.method, 17 | data: body 18 | }); 19 | 20 | if (options.successMessage) toast.success(options.successMessage); 21 | 22 | setData(data); 23 | if (options.onSuccess) await options.onSuccess(data); 24 | } catch (e) { 25 | if (!(e instanceof AxiosError)) { 26 | toast.error("알 수 없는 에러가 발생하였습니다."); 27 | return; 28 | } 29 | 30 | if (e.response && e.response.status >= 500) { 31 | toast.error("알 수 없는 에러가 발생했습니다"); 32 | } else if (typeof errors === "string") { 33 | toast.error(options.errors); 34 | } else if ( 35 | options.errors && 36 | e.response && 37 | options.errors[e.response.status] 38 | ) { 39 | toast.error(options.errors[e.response.status]); 40 | } 41 | 42 | if (options.onFailure) await options.onFailure(e); 43 | } finally { 44 | setIsLoading(false); 45 | } 46 | }, 47 | [options] 48 | ); 49 | 50 | return { fetch, isLoading, data }; 51 | }; 52 | 53 | export default useFetch; 54 | -------------------------------------------------------------------------------- /src/pages/Project/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as C from "../../components"; 3 | import * as S from "./style"; 4 | import GetRole from "../../lib/GetRole"; 5 | import useBoard from "../../Hooks/useBoard"; 6 | 7 | export default function Project() { 8 | const { boardList } = useBoard({ boardType: "PROJECT" }); 9 | 10 | if (!boardList) return null; 11 | 12 | const projects = [ 13 | { title: "팀", type: "TEAM" }, 14 | { title: "개인", type: "INDIVIDUAL" } 15 | ]; 16 | 17 | const handleBoardItemClick = boardId => { 18 | const boardUrl = `/board/${boardId}`; 19 | window.location.href = boardUrl; 20 | }; 21 | 22 | const renderBoardItems = projectType => { 23 | const filteredItems = boardList 24 | .filter(item => item.boardDetailType === projectType) 25 | .sort((a, b) => a.title.localeCompare(b.title, "en")); 26 | 27 | return filteredItems.map(item => ( 28 | 29 | 30 | handleBoardItemClick(item.id)}> 31 | {item.title} 32 | 33 | 34 | 35 | )); 36 | }; 37 | 38 | return ( 39 | 46 | {projects.map(project => ( 47 | 48 | {renderBoardItems(project.type)} 49 | 50 | ))} 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/pages/InquiryDetail/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import * as C from "../../components"; 3 | import { useFetch } from "../../Hooks"; 4 | import { useNavigate, useParams } from "react-router-dom"; 5 | import GetRole from "../../lib/GetRole"; 6 | import { toast } from "react-toastify"; 7 | 8 | const InquiryDetail = () => { 9 | const [state, setState] = useState({ 10 | id: "", 11 | content: "", 12 | title: "", 13 | name: "", 14 | inquiryType: "" 15 | }); 16 | 17 | let { id } = useParams(); 18 | 19 | const { fetch } = useFetch({ 20 | url: `/inquiry/${id}`, 21 | method: "get", 22 | onSuccess: data => { 23 | setState(data); 24 | }, 25 | erros: { 26 | 400: "글을 불러오지 못했습니다." 27 | } 28 | }); 29 | 30 | useEffect(() => { 31 | fetch(); 32 | }, []); 33 | 34 | const navigate = useNavigate(); 35 | const role = GetRole(); 36 | 37 | useEffect(() => { 38 | if (role !== "관리자") { 39 | toast.error("권한이 없습니다."); 40 | navigate("/"); 41 | } 42 | }, [role, navigate]); 43 | 44 | const formattedCreatedDate = new Date(state.createdDate).toLocaleString(); 45 | 46 | return ( 47 | 48 | 49 | 56 | 57 | 58 | ); 59 | }; 60 | 61 | export default InquiryDetail; 62 | -------------------------------------------------------------------------------- /src/pages/Event/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useBoard from "../../Hooks/useBoard"; 3 | import * as C from "../../components"; 4 | import * as S from "./style"; 5 | 6 | export default function Student() { 7 | const { boardList } = useBoard({ boardType: "INCIDENT" }); 8 | 9 | if (!boardList) return null; 10 | 11 | const years = [ 12 | { title: "2024", type: "TWENTY_FOURTH" }, 13 | { title: "2023", type: "TWENTY_THIRD" }, 14 | { title: "2022", type: "TWENTY_SECOND" }, 15 | { title: "2021", type: "TWENTY_FIRST" } 16 | ]; 17 | 18 | const renderBoardItems = yearType => { 19 | const filteredItems = boardList 20 | .filter(item => item.boardDetailType === yearType) 21 | .sort((a, b) => a.title.localeCompare(b.title, "ko")); 22 | 23 | const handleBoardItemClick = boardId => { 24 | const boardUrl = `/board/${boardId}`; 25 | window.location.href = boardUrl; 26 | }; 27 | 28 | return filteredItems.map(item => ( 29 | 30 | 31 | handleBoardItemClick(item.id)}> 32 | {item.title} 33 | 34 | 35 | 36 | )); 37 | }; 38 | 39 | return ( 40 | 47 | {years.map(year => ( 48 | 49 | {renderBoardItems(year.type)} 50 | 51 | ))} 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/assets/CodeIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function CodeIcon() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default CodeIcon; 21 | -------------------------------------------------------------------------------- /src/pages/Schedule/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as C from "../../components"; 3 | import * as S from "./style"; 4 | import GetRole from "../../lib/GetRole"; 5 | import useBoard from "../../Hooks/useBoard"; 6 | 7 | export default function Student() { 8 | const role = GetRole(); 9 | const { boardList } = useBoard({ boardType: "SCHEDULE" }); 10 | 11 | if (!boardList) return null; 12 | 13 | const months = [ 14 | "JAN", 15 | "FEB", 16 | "MAR", 17 | "APR", 18 | "MAY", 19 | "JUN", 20 | "JUL", 21 | "AUG", 22 | "SEPT", 23 | "OCT", 24 | "NOV", 25 | "DEC" 26 | ]; 27 | 28 | const handleBoardItemClick = boardId => { 29 | const boardUrl = `/board/${boardId}`; 30 | window.location.href = boardUrl; 31 | }; 32 | 33 | const renderBoardItems = month => { 34 | const filteredItems = boardList 35 | .filter(item => item.boardDetailType === month) 36 | .sort((a, b) => a.title.localeCompare(b.title, "ko")); 37 | 38 | return filteredItems.map(item => ( 39 | 40 | 41 | handleBoardItemClick(item.id)}> 42 | {item.title} 43 | 44 | 45 | 46 | )); 47 | }; 48 | 49 | return ( 50 | 57 | {months.map((month, index) => ( 58 | 59 | {renderBoardItems(month)} 60 | 61 | ))} 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/components/Refusal/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ModalOverlay = styled.div` 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | width: 100vw; 8 | height: 100vh; 9 | background-color: rgba(0, 0, 0, 0.3); 10 | z-index: 100; 11 | position: fixed; 12 | `; 13 | 14 | export const ModalBox = styled.div` 15 | z-index: 101; 16 | position: fixed; 17 | top: 50%; 18 | left: 50%; 19 | transform: translate(-50%, -50%); 20 | `; 21 | 22 | export const RefusalContainer = styled.div` 23 | width: 400px; 24 | height: 520px; 25 | background-color: #fff; 26 | border-radius: 10px; 27 | position: relative; 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | flex-direction: column; 32 | z-index: 101; 33 | 34 | & > svg { 35 | position: absolute; 36 | right: 16px; 37 | top: 16px; 38 | cursor: pointer; 39 | } 40 | `; 41 | export const RefusalContent = styled.p` 42 | font-size: 1.4rem; 43 | text-align: center; 44 | font-weight: 600; 45 | display: flex; 46 | margin-top: 1vw; 47 | `; 48 | 49 | export const InputRefusal = styled.textarea` 50 | height: 60%; 51 | width: 80%; 52 | outline: none; 53 | border: none; 54 | border-radius: 4px; 55 | font-size: 14px; 56 | padding: 16px; 57 | color: #575757; 58 | resize: none; 59 | background-color: #f1f1f5; 60 | margin-top: 1.8vw; 61 | `; 62 | 63 | export const YesButton = styled.button` 64 | color: #007eff; 65 | background: none; 66 | cursor: pointer; 67 | width: 80%; 68 | height: 10%; 69 | background-color: #007eff; 70 | color: #fff; 71 | border: none; 72 | border-radius: 4px; 73 | font-size: 14px; 74 | font-weight: 600; 75 | margin-top: 2vw; 76 | `; 77 | -------------------------------------------------------------------------------- /src/components/Graph/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const GraphCenter = styled.div` 4 | width: 44vw; 5 | display: flex; 6 | @media screen and (max-width: 1280px) { 7 | width: 100%; 8 | } 9 | `; 10 | 11 | export const TitleGraph = styled.div` 12 | width: 24%; 13 | border: 1px solid #c0c0c0; 14 | display: flex; 15 | height: 40px; 16 | justify-content: center; 17 | align-items: center; 18 | background-color: ${props => props.backgroundColor}; 19 | color: white; 20 | font-weight: bold; 21 | font-size: 0.98rem; 22 | word-break: break-all; 23 | font-size: 0.9rem; 24 | 25 | @media screen and (max-width: 1400px) { 26 | font-size: 0.8rem; 27 | } 28 | 29 | @media screen and (max-width: 800px) { 30 | font-size: 0.2rem; 31 | height: 100%; 32 | } 33 | @media screen and (max-width: 700px) { 34 | font-size: 0.1rem; 35 | width: 35%; 36 | } 37 | `; 38 | 39 | export const ContentGraph = styled.div` 40 | width: 100%; 41 | border: 1px solid #dddddd; 42 | border-left: 0; 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | font-size: ${props => (props.contentColor ? "0.98rem" : "0.85rem")}; 47 | color: ${props => (props.color ? "white" : "black")}; 48 | font-weight: ${props => (props.contentColor ? "700" : true)}; 49 | background-color: ${props => (props.contentColor ? "#007EFF" : "white")}; 50 | 51 | @media screen and (max-width: 1300px) { 52 | font-size: 0.8rem; 53 | } 54 | 55 | @media screen and (max-width: 800px) { 56 | font-size: 0.2rem; 57 | height: 100%; 58 | } 59 | @media screen and (max-width: 700px) { 60 | font-size: 0.1rem; 61 | } 62 | `; 63 | -------------------------------------------------------------------------------- /src/components/RecentModified/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const RecentModifiedContainer = styled.div` 4 | position: sticky; 5 | display: flex; 6 | flex-direction: column; 7 | font-weight: 700; 8 | width: 12vw; 9 | height: fit-content; 10 | top: 6vw; 11 | 12 | @media screen and (max-width: 1000px) { 13 | display: none; 14 | } 15 | `; 16 | 17 | export const Title = styled.div` 18 | color: #ffffff; 19 | background-color: #007eff; 20 | line-height: 5vh; 21 | width: 15vw; 22 | height: 5vh; 23 | font-size: 1.2rem; 24 | padding-left: 22px; 25 | `; 26 | 27 | export const ModifiedItem = styled.div` 28 | width: 15vw; 29 | height: 4.5vh; 30 | background-color: #ffffff; 31 | display: flex; 32 | justify-content: space-between; 33 | align-items: center; 34 | padding: 0 22px; 35 | border: solid 1px #c0c0c0; 36 | border-top: none; 37 | cursor: pointer; 38 | `; 39 | 40 | export const ModifiedItemTitle = styled.h3` 41 | font-size: 1rem; 42 | font-weight: 600; 43 | padding: 4px; 44 | color: #191919; 45 | text-overflow: ellipsis; 46 | overflow: hidden; 47 | word-break: break-all; 48 | white-space: nowrap; 49 | 50 | @media screen and (max-width: 1600px) { 51 | font-size: 0.8rem; 52 | } 53 | 54 | @media screen and (max-width: 1450px) { 55 | font-size: 0.7rem; 56 | } 57 | 58 | @media screen and (max-width: 1200px) { 59 | font-size: 0.4rem; 60 | } 61 | `; 62 | 63 | export const ModifiedItemTime = styled.p` 64 | font-size: 0.8rem; 65 | white-space: nowrap; 66 | color: #999999; 67 | 68 | @media screen and (max-width: 1450px) { 69 | font-size: 0.7rem; 70 | } 71 | 72 | @media screen and (max-width: 1200px) { 73 | font-size: 0.4rem; 74 | } 75 | `; 76 | -------------------------------------------------------------------------------- /src/pages/Teacher/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as C from "../../components"; 3 | import * as S from "./style"; 4 | import GetRole from "../../lib/GetRole"; 5 | import useBoard from "../../Hooks/useBoard"; 6 | 7 | export default function Student() { 8 | const role = GetRole(); 9 | const { boardList } = useBoard({ boardType: "TEACHER" }); 10 | 11 | if (!boardList) return null; 12 | 13 | const boardTypes = [ 14 | { title: "일반 교과 선생님", type: "GENERAL" }, 15 | { title: "전문 교과 선생님", type: "SPECIALITY" }, 16 | { title: "기타 부서 선생님", type: "OTHER" } 17 | ]; 18 | 19 | const renderBoardItems = boardType => { 20 | const filteredItems = boardList 21 | .filter(item => item.boardDetailType === boardType) 22 | .sort((a, b) => a.title.localeCompare(b.title, "ko")); 23 | 24 | const handleBoardItemClick = boardId => { 25 | const boardUrl = `/board/${boardId}`; 26 | window.location.href = boardUrl; 27 | }; 28 | 29 | return filteredItems.map(item => ( 30 | 31 | 32 | handleBoardItemClick(item.id)}> 33 | {item.title} 34 | 35 | 36 | 37 | )); 38 | }; 39 | 40 | return ( 41 | 48 | {boardTypes.map(boardType => ( 49 | 54 | {renderBoardItems(boardType.type)} 55 | 56 | ))} 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/Hooks/useEdit.js: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | import { useCallback } from "react"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { toast } from "react-toastify"; 5 | import API from "../apis"; 6 | 7 | const useEdit = ({ props }) => { 8 | const navigate = useNavigate(); 9 | 10 | const editBoardUpload = useCallback(async () => { 11 | try { 12 | await API.patch(`/board/${props.id}`, { 13 | title: props.editTitle, 14 | content: props.editContent 15 | }); 16 | 17 | toast.success("편집 되었습니다."); 18 | navigate(`/board/${props.id}`); 19 | } catch (e) { 20 | if (!(e instanceof AxiosError)) { 21 | toast.error("편집에 실패하였습니다."); 22 | return; 23 | } 24 | if (e.response && e.response.status >= 500) { 25 | toast.error("편집에 실패하였습니다."); 26 | } else if (e.response && e.response.status >= 409) { 27 | toast.error("이미 존재하는 제목입니다."); 28 | } else if (e.response && e.response.status >= 400) { 29 | toast.error("변경 내용이 없습니다."); 30 | } 31 | } 32 | }, [props, navigate]); 33 | 34 | const editNoticeUpload = useCallback(async () => { 35 | try { 36 | await API.patch(`/notice/${props.id}`, { 37 | title: props.editTitle, 38 | content: props.editContent 39 | }); 40 | 41 | toast.success("편집 되었습니다."); 42 | navigate(`/notice/${props.id}`); 43 | } catch (e) { 44 | if (!(e instanceof AxiosError)) { 45 | toast.error("이미 존재하는 제목입니다."); 46 | return; 47 | } 48 | if (e.response && e.response.status >= 409) { 49 | toast.error("이미 존재하는 제목입니다."); 50 | } 51 | } 52 | }, [props, navigate]); 53 | 54 | return { editBoardUpload, editNoticeUpload }; 55 | }; 56 | 57 | export default useEdit; 58 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Button } from "./Button"; 2 | export { default as Header } from "./Header"; 3 | export { default as Logout } from "./Logout"; 4 | export { default as PageContainer } from "./PageContainer"; 5 | export { default as Explanation } from "./Explanation"; 6 | export { default as Footer } from "./Footer"; 7 | export { default as Detail } from "./Detail"; 8 | export { default as ScrollButton } from "./ScrollButton"; 9 | export { default as WriteBox } from "./WriteBox"; 10 | export { default as EditWriteBox } from "./WriteBox/EditWriteBox"; 11 | export { default as PreviewWriteBox } from "./WriteBox/PreviewWriteBox"; 12 | export { default as MarkDownConverter } from "./WriteBox/PreviewWriteBox/MarkdownConverter"; 13 | export { default as Graph } from "./Graph"; 14 | export { default as ContentsButton } from "./ContentsButton"; 15 | export { default as RecentModified } from "./RecentModified"; 16 | export { default as InquiryItem } from "./InquiryItem"; 17 | export { default as InquiryDetailItem } from "./InquiryDetailItem"; 18 | export { default as InquiryWrite } from "./InquiryWrite"; 19 | export { default as WriteOption } from "./WriteOption"; 20 | export { default as HistoryItem } from "./HistoryItem"; 21 | export { default as HistoryDetail } from "./HistoryDetailItem"; 22 | export { default as NoticeWrite } from "./NoticeWrite"; 23 | export { default as Refusal } from "./Refusal"; 24 | export { default as NoticeDetail } from "./NoticeDetail"; 25 | export { default as BoardDetail } from "./BoardDetail"; 26 | export { default as EditWrite } from "./EditWrite"; 27 | export { default as EditNotice } from "./EditNotice"; 28 | export { default as PromotionPage } from "./PromotionPage"; 29 | export { default as InquiryModal } from "./InquiryModal"; 30 | -------------------------------------------------------------------------------- /src/pages/Student/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as C from "../../components"; 3 | import * as S from "./style"; 4 | import GetRole from "../../lib/GetRole"; 5 | import useBoard from "../../Hooks/useBoard"; 6 | 7 | export default function Student() { 8 | const role = GetRole(); 9 | const { boardList } = useBoard({ boardType: "STUDENT" }); 10 | 11 | if (!boardList) return null; 12 | 13 | const generations = [ 14 | { title: "5기", type: "FIFTH" }, 15 | { title: "6기", type: "SIXTH" }, 16 | { title: "7기", type: "SEVENTH" }, 17 | { title: "8기", type: "EIGHTH" } 18 | ]; 19 | 20 | const handleBoardItemClick = boardId => { 21 | const boardUrl = `/board/${boardId}`; 22 | window.location.href = boardUrl; 23 | }; 24 | 25 | const renderBoardItems = generationType => { 26 | const filteredItems = boardList 27 | .filter(item => item.boardDetailType === generationType) 28 | .sort((a, b) => a.title.localeCompare(b.title, "ko")); 29 | 30 | return filteredItems.map(item => ( 31 | 32 | 33 | handleBoardItemClick(item.id)}> 34 | {item.title} 35 | 36 | 37 | 38 | )); 39 | }; 40 | 41 | return ( 42 | 49 | {generations.map(generation => ( 50 | 55 | {renderBoardItems(generation.type)} 56 | 57 | ))} 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/components/InquiryDetailItem/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import * as S from "./style"; 3 | import * as C from "../index"; 4 | import { useMail } from "../../Hooks"; 5 | import useMarkdown from "../../Hooks/useMarkdown"; 6 | import DOMPurify from "dompurify"; 7 | 8 | const InquiryDetailItem = ({ id, content, name, inquiryType, createdDate }) => { 9 | const { postApproveMail } = useMail({ props: { id } }); 10 | 11 | const [showRefusal, setShowRefusal] = useState(false); 12 | 13 | const handleApproveMail = () => { 14 | const shouldApprove = window.confirm("정말로 승인하시겠습니까?"); 15 | 16 | if (shouldApprove) { 17 | postApproveMail(); 18 | } 19 | }; 20 | 21 | const handleRefusalMail = () => { 22 | setShowRefusal(true); 23 | }; 24 | 25 | const { markdownToHtml } = useMarkdown(); 26 | 27 | const html = markdownToHtml(content); 28 | 29 | const cleanHtml = DOMPurify.sanitize(html); 30 | 31 | return ( 32 | <> 33 | 34 | 작성자 : {name} 35 | 작성일 : {createdDate} 36 | 37 | 38 | {showRefusal && ( 39 | 44 | )} 45 | 46 | { 48 | handleApproveMail(); 49 | }} 50 | > 51 | 승인 52 | 53 | { 55 | handleRefusalMail(); 56 | }} 57 | > 58 | 거절 59 | 60 | 61 | 62 | ); 63 | }; 64 | 65 | export default InquiryDetailItem; 66 | -------------------------------------------------------------------------------- /src/components/Logout/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ModalOverlay = styled.div` 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | width: 100vw; 8 | height: 100vh; 9 | background-color: rgba(0, 0, 0, 0.3); 10 | z-index: 100; 11 | position: fixed; 12 | `; 13 | 14 | export const ModalBox = styled.div` 15 | z-index: 101; 16 | position: fixed; 17 | top: 50%; 18 | left: 50%; 19 | transform: translate(-50%, -50%); 20 | `; 21 | 22 | export const LogoutContainer = styled.div` 23 | width: 320px; 24 | height: 440px; 25 | background-color: #fff; 26 | border-radius: 10px; 27 | position: relative; 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | flex-direction: column; 32 | z-index: 101; 33 | 34 | & > svg { 35 | position: absolute; 36 | right: 16px; 37 | top: 16px; 38 | cursor: pointer; 39 | } 40 | `; 41 | export const LogoutContent = styled.p` 42 | font-size: 18px; 43 | text-align: center; 44 | margin-top: 24px; 45 | `; 46 | 47 | export const LogoutTitle = styled.div` 48 | display: flex; 49 | flex-direction: column; 50 | align-items: center; 51 | 52 | & > svg { 53 | margin-top: 20px; 54 | } 55 | `; 56 | 57 | export const BtnContainer = styled.div` 58 | display: flex; 59 | flex-direction: column; 60 | margin-top: 52px; 61 | `; 62 | 63 | export const NoButton = styled.button` 64 | color: #fff; 65 | background-color: #007eff; 66 | cursor: pointer; 67 | width: 15vw; 68 | height: 6vh; 69 | border-radius: 10px; 70 | border: none; 71 | font-size: 16px; 72 | `; 73 | 74 | export const YesButton = styled.button` 75 | color: #007eff; 76 | background: none; 77 | cursor: pointer; 78 | width: 15vw; 79 | height: 6vh; 80 | border-radius: 10px; 81 | border: 1px solid #007eff; 82 | margin-top: 12px; 83 | font-size: 16px; 84 | `; 85 | -------------------------------------------------------------------------------- /src/assets/H4Icon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function H4Icon() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default H4Icon; 21 | -------------------------------------------------------------------------------- /src/components/Header/dropMenu/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./style"; 3 | import { Link } from "react-router-dom"; 4 | import GetRole from "../../../lib/GetRole"; 5 | 6 | function DropMenu({ onMouseEnter, onMouseLeave }) { 7 | const data = GetRole(); 8 | 9 | return ( 10 | 11 | 12 | 13 | 14 | 공지사항 15 | 16 | 17 | 문의 18 | 19 | {data === "관리자" ? ( 20 | 21 | 문의 리스트 22 | 23 | ) : null} 24 | {data === "관리자" ? ( 25 | 26 | 권한 부여 27 | 28 | ) : null} 29 | 30 | 31 | 32 | 학생 33 | 34 | 35 | 선생님 36 | 37 | 38 | 동아리 39 | 40 | 41 | 전공 42 | 43 | 44 | 45 | 46 | 사건 47 | 48 | 49 | 학사일정 50 | 51 | 52 | 프로젝트 53 | 54 | 55 | 깃허브 56 | 57 | 58 | 59 | 60 | ); 61 | } 62 | 63 | export default DropMenu; 64 | -------------------------------------------------------------------------------- /src/components/WriteOption/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const OptionContainer = styled.div` 4 | display: flex; 5 | align-items: center; 6 | border: 1px solid #c0c0c0; 7 | width: 25.4rem; 8 | background-color: none; 9 | margin-bottom: 8px; 10 | padding: 3px 10px; 11 | position: relative; 12 | 13 | div { 14 | display: inline-block; 15 | width: 100%; 16 | 17 | svg { 18 | margin-left: 12px; 19 | cursor: pointer; 20 | &:hover { 21 | transform: scale(1.275); 22 | } 23 | } 24 | 25 | span { 26 | display: none; 27 | position: absolute; 28 | bottom: 140%; 29 | max-width: 200px; 30 | padding: 5px 10px; 31 | -webkit-border-radius: 8px; 32 | -moz-border-radius: 8px; 33 | border-radius: 3px; 34 | background: #808080; 35 | color: #fff; 36 | font-size: 0.8rem; 37 | z-index: 99; 38 | text-align: center; 39 | 40 | &::after { 41 | position: absolute; 42 | top: 99%; 43 | left: 50%; 44 | margin-left: -5px; 45 | border: solid transparent; 46 | border-color: rgba(51, 51, 51, 0); 47 | border-bottom-color: #808080; 48 | border-width: 5px; 49 | pointer-events: none; 50 | transform: rotate(180deg); 51 | content: " "; 52 | } 53 | } 54 | 55 | &:hover { 56 | span { 57 | display: block; 58 | } 59 | } 60 | 61 | .imgTooltip { 62 | width: 200px; 63 | left: 63%; 64 | 65 | } 66 | 67 | .codeTooltip { 68 | width: 70px; 69 | left: 86.7%; 70 | } 71 | } 72 | 73 | .unFunctionIcon { 74 | svg { 75 | margin-left: 12px; 76 | cursor: default; 77 | width: 5px; 78 | 79 | &:hover { 80 | transform: scale(1); 81 | } 82 | } 83 | } 84 | 85 | label { 86 | display: flex; 87 | justify-content: center; 88 | align-items: center; 89 | } 90 | 91 | input { 92 | display: none; 93 | } 94 | `; 95 | -------------------------------------------------------------------------------- /src/Hooks/useNotice.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { useNavigate, useParams } from "react-router-dom"; 3 | import { toast } from "react-toastify"; 4 | import API from "../apis"; 5 | import useFetch from "./useFetch"; 6 | 7 | const useNotice = ({ props }) => { 8 | const navigate = useNavigate(); 9 | 10 | const uploadNotice = useCallback(async () => { 11 | try { 12 | await API.post("/notice", { 13 | title: props.title, 14 | content: props.content 15 | }); 16 | 17 | toast.success("공지가 성공적으로 등록되었습니다."); 18 | navigate("/notice"); 19 | } catch (e) { 20 | if (e.response && e.response.status >= 403) { 21 | toast.error("권한이 없습니다."); 22 | } else if (e.response && e.response.status >= 401) { 23 | toast.error("로그인이 필요합니다."); 24 | } else { 25 | toast.error("공지 글을 작성할 수 없습니다."); 26 | } 27 | } 28 | }, [props.title, props.content, navigate]); 29 | 30 | const deleteNotice = useCallback(async () => { 31 | try { 32 | await API.delete(`/notice/${props.id}`); 33 | 34 | toast.success("공지가 성공적으로 지워졌습니다."); 35 | navigate("/notice"); 36 | } catch (e) { 37 | if (e.response && e.response.status >= 403) { 38 | toast.error("권한이 없습니다."); 39 | } else if (e.response && e.response.status >= 401) { 40 | toast.error("로그인이 필요합니다."); 41 | } else { 42 | toast.error("공지 글을 삭제할 수 없습니다."); 43 | } 44 | } 45 | }, [props.id, navigate]); 46 | 47 | const [state, setState] = useState({ 48 | id: "", 49 | content: "", 50 | title: "", 51 | createdDate: "", 52 | editedDate: "" 53 | }); 54 | 55 | let { id } = useParams(); 56 | 57 | const { fetch } = useFetch({ 58 | url: `/notice/${id}`, 59 | method: "get", 60 | onSuccess: data => { 61 | setState(data); 62 | }, 63 | erros: { 64 | 400: "글을 불러오지 못했습니다." 65 | } 66 | }); 67 | 68 | useEffect(() => { 69 | fetch(); 70 | }, []); 71 | 72 | return { uploadNotice, deleteNotice, state }; 73 | }; 74 | 75 | export default useNotice; 76 | -------------------------------------------------------------------------------- /src/pages/Inquiry/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import * as C from "../../components"; 3 | import * as S from "./style"; 4 | import { useFetch } from "../../Hooks"; 5 | import { Link, useNavigate } from "react-router-dom"; 6 | import GetRole from "../../lib/GetRole"; 7 | import { toast } from "react-toastify"; 8 | 9 | export default function Inquiry() { 10 | const [inquiryList, setInquiryList] = useState([]); 11 | 12 | const { fetch } = useFetch({ 13 | url: `/inquiry`, 14 | method: "get", 15 | onSuccess: data => { 16 | const sortedInquiryList = data.inquiryList.sort((a, b) => 17 | b.createdDate.localeCompare(a.createdDate) 18 | ); 19 | setInquiryList(sortedInquiryList); 20 | }, 21 | errors: { 22 | 400: "문의 정보를 가져오지 못함" 23 | } 24 | }); 25 | 26 | useEffect(() => { 27 | fetch(); 28 | }, []); 29 | 30 | const navigate = useNavigate(); 31 | const role = GetRole(); 32 | 33 | useEffect(() => { 34 | if (role !== "관리자") { 35 | toast.error("권한이 없습니다."); 36 | navigate("/"); 37 | } 38 | }, [role, navigate]); 39 | 40 | return ( 41 | 42 | {inquiryList.map(item => ( 43 | 44 | 45 | 46 | 47 | 48 | {item.title} 49 | {item.inquiryType} 50 | {item.createdDate.substring(0, 10)} 51 | 52 | 53 | {item.content.length > 100 54 | ? item.content.substring(0, 120) + "..." 55 | : item.content} 56 | 57 | 58 | 59 | 60 | 61 | ))} 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/components/WriteBox/EditWriteBox/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const EditWriteBoxContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | align-items: flex-start; 7 | 8 | span { 9 | color: #999999; 10 | font-size: 1rem; 11 | font-weight: 500; 12 | } 13 | `; 14 | 15 | export const CategoryInputBox = styled.div` 16 | display: flex; 17 | align-items: center; 18 | justify-content: space-between; 19 | width: ${props => (props.type === "문의목적" ? "14.8vw" : "26vw")}; 20 | margin-bottom: 10px; 21 | 22 | select { 23 | border: 1px solid #dddddd; 24 | width: 10vw; 25 | height: 25px; 26 | outline: none; 27 | color: #999999; 28 | font-weight: 300; 29 | } 30 | `; 31 | 32 | export const TitleInputBox = styled.div` 33 | display: flex; 34 | align-items: center; 35 | justify-content: space-between; 36 | width: 60vw; 37 | margin-bottom: 10px; 38 | 39 | span { 40 | margin-left: 12px; 41 | } 42 | 43 | input { 44 | border: 1px solid #dddddd; 45 | width: 55.2vw; 46 | height: 25px; 47 | padding-left: 8px; 48 | outline: none; 49 | color: #191919; 50 | } 51 | `; 52 | 53 | export const ContentInputBox = styled.div` 54 | display: flex; 55 | align-items: center; 56 | justify-content: space-between; 57 | width: 60vw; 58 | 59 | textarea { 60 | width: 55.2vw; 61 | height: auto; 62 | padding: 8px; 63 | color: #191919; 64 | border: 1px solid #dddddd; 65 | font-size: 0.8rem; 66 | outline: none; 67 | resize: none; 68 | line-height: 22px; 69 | overflow-y: hidden; 70 | } 71 | `; 72 | 73 | export const LineNumberBox = styled.div` 74 | display: flex; 75 | flex-direction: column; 76 | align-items: center; 77 | line-height: 22px; 78 | margin-left: 20px; 79 | `; 80 | 81 | 82 | export const AlarmBox = styled.div` 83 | display: flex; 84 | justify-content: space-between; 85 | align-items: center; 86 | font-size: 0.8rem; 87 | width: 285px; 88 | margin-left: 75px; 89 | margin-top: 15px; 90 | color: #636363; 91 | font-weight: 300; 92 | ` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gmu-wiki-front-v2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.10.6", 7 | "@fortawesome/fontawesome-svg-core": "^6.4.0", 8 | "@fortawesome/free-brands-svg-icons": "^6.4.0", 9 | "@fortawesome/free-regular-svg-icons": "^6.4.0", 10 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 11 | "@fortawesome/react-fontawesome": "^0.2.0", 12 | "@msg-team/gauth-react": "^1.4.1", 13 | "@reduxjs/toolkit": "^1.9.5", 14 | "@testing-library/jest-dom": "^5.16.5", 15 | "@testing-library/react": "^13.4.0", 16 | "@testing-library/user-event": "^13.5.0", 17 | "@vercel/analytics": "^1.0.2", 18 | "axios": "^1.4.0", 19 | "dompurify": "^3.0.5", 20 | "jsonwebtoken": "^9.0.1", 21 | "jwt-decode": "^3.1.2", 22 | "marked": "^5.1.0", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-redux": "^8.1.1", 26 | "react-responsive": "^9.0.2", 27 | "react-router-dom": "^6.14.2", 28 | "react-scripts": "5.0.1", 29 | "react-textarea-autosize": "^8.5.2", 30 | "react-toastify": "^9.1.3", 31 | "redux": "^4.2.1", 32 | "styled-components": "^5.3.9", 33 | "web-vitals": "^2.1.4" 34 | }, 35 | "scripts": { 36 | "start": "react-scripts start", 37 | "build": "react-scripts build", 38 | "test": "react-scripts test", 39 | "eject": "react-scripts eject" 40 | }, 41 | "eslintConfig": { 42 | "extends": [ 43 | "react-app", 44 | "react-app/jest" 45 | ] 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "description": "This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).", 60 | "main": "index.js", 61 | "devDependencies": { 62 | "webpack-bundle-analyzer": "^4.9.1" 63 | }, 64 | "keywords": [], 65 | "author": "", 66 | "license": "ISC" 67 | } 68 | -------------------------------------------------------------------------------- /src/components/RecentModified/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useFetch } from "../../Hooks"; 3 | import * as S from "./style"; 4 | 5 | export default function RecentModified() { 6 | const [recentList, setRecentList] = useState([]); 7 | 8 | const { fetch } = useFetch({ 9 | url: `/board/recent`, 10 | method: "get", 11 | onSuccess: data => { 12 | setRecentList(data.boardTitleList); 13 | }, 14 | errors: { 15 | 403: "권한이 없습니다.", 16 | 500: "목록을 불러올 수 없습니다." 17 | } 18 | }); 19 | 20 | useEffect(() => { 21 | fetch(); 22 | }, []); 23 | 24 | function formatTime(timeString) { 25 | const date = new Date(timeString); 26 | const now = new Date(); 27 | 28 | const elapsedMilliseconds = now - date; 29 | const elapsedSeconds = Math.floor(elapsedMilliseconds / 1000); 30 | const elapsedMinutes = Math.floor(elapsedSeconds / 60); 31 | const elapsedHours = Math.floor(elapsedMinutes / 60); 32 | const elapsedDays = Math.floor(elapsedHours / 24); 33 | const elapsedMonths = Math.floor(elapsedDays / 30); 34 | 35 | if (elapsedMonths > 0) { 36 | return `${elapsedMonths}개월 전`; 37 | } else if (elapsedDays > 0) { 38 | return `${elapsedDays}일 전`; 39 | } else if (elapsedHours > 0) { 40 | return `${elapsedHours}시간 전`; 41 | } else if (elapsedMinutes > 0) { 42 | return `${elapsedMinutes}분 전`; 43 | } else { 44 | return `방금 전`; 45 | } 46 | } 47 | 48 | const handleBoardItemClick = boardId => { 49 | const boardUrl = `/board/${boardId}`; 50 | window.location.href = boardUrl; 51 | }; 52 | 53 | return ( 54 | 55 | 최근 생성 / 수정된 글 56 | {recentList.map(({ id, title, editedDate }) => ( 57 | 58 | handleBoardItemClick(id)}> 59 | {title} 60 | {formatTime(editedDate)} 61 | 62 | 63 | ))} 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/components/InquiryWrite/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const WriteOptions = styled.div` 4 | width: 100%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | height: 4.7vh; 9 | `; 10 | 11 | export const WriteBoxContainer = styled.div` 12 | display: flex; 13 | flex-direction: column; 14 | ` 15 | 16 | export const ChangeButtonContainer = styled.div` 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | z-index: 99; 21 | `; 22 | 23 | export const EditButton = styled.button` 24 | width: 4.6vw; 25 | height: 5vh; 26 | background: ${props => (props.checked ? "#ffffff" : "none")}; 27 | border: ${props => (props.checked ? "1px solid #c0c0c0" : "none")}; 28 | border-bottom: ${props => (props.checked ? "none" : "")}; 29 | color: #999999; 30 | cursor: pointer; 31 | font-size: 1rem; 32 | outline: none; 33 | 34 | @media screen and (max-width: 1400px) { 35 | font-size: 0.9rem; 36 | } 37 | 38 | @media screen and (max-width: 800px) { 39 | font-size: 0.8rem; 40 | } 41 | `; 42 | 43 | export const PreviewButton = styled.button` 44 | width: 5vw; 45 | height: 5vh; 46 | color: #007eff; 47 | background: ${props => (props.checked ? "#ffffff" : "none")}; 48 | border: ${props => (props.checked ? "1px solid #c0c0c0" : "none")}; 49 | border-bottom: ${props => props.checked && "1px solid #ffffff"}; 50 | cursor: pointer; 51 | font-size: 1rem; 52 | outline: none; 53 | 54 | @media screen and (max-width: 1400px) { 55 | font-size: 0.9rem; 56 | } 57 | 58 | @media screen and (max-width: 800px) { 59 | font-size: 0.8rem; 60 | } 61 | `; 62 | 63 | export const WriteBox = styled.div` 64 | background-color: none; 65 | overflow-y: auto; 66 | border: 1px solid #c0c0c0; 67 | width: 100%; 68 | height: auto; 69 | display: flex; 70 | flex-direction: column; 71 | padding: 20px; 72 | `; 73 | 74 | export const RegisterButton = styled.button` 75 | border: none; 76 | outline: none; 77 | color: #ffffff; 78 | background-color: #007eff; 79 | width: 8vw; 80 | height: 5vh; 81 | font-size: 1rem; 82 | margin: 20px 0; 83 | float: right; 84 | cursor: pointer; 85 | `; -------------------------------------------------------------------------------- /src/router/index.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Route, Routes, useNavigate } from "react-router-dom"; 3 | import TokenManager from "../apis/TokenManager"; 4 | import * as P from "../pages"; 5 | import EnvConfig from "../apis/EnvConfig"; 6 | 7 | export default function Router() { 8 | const tokenManager = new TokenManager(); 9 | const accessToken = tokenManager.accessToken; 10 | const navigate = useNavigate(); 11 | 12 | useEffect(() => { 13 | if (!accessToken) { 14 | navigate("/promotion"); 15 | console.log(EnvConfig.GMUWIKI_SERVER_URL); 16 | } else if (window.location.pathname === "/promotion") { 17 | navigate("/"); 18 | } 19 | }, [accessToken, navigate]); 20 | 21 | return ( 22 | 23 | } /> 24 | } /> 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | } /> 30 | } /> 31 | } /> 32 | } /> 33 | } /> 34 | } /> 35 | } /> 36 | } /> 37 | } /> 38 | } /> 39 | } /> 40 | } /> 41 | } /> 42 | } /> 43 | } /> 44 | } /> 45 | } /> 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/EditNotice/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const WriteOptions = styled.div` 4 | width: 100%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | height: 4.7vh; 9 | `; 10 | 11 | export const WriteBoxContainer = styled.div` 12 | display: flex; 13 | flex-direction: column; 14 | ` 15 | 16 | export const ChangeButtonContainer = styled.div` 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | z-index: 99; 21 | `; 22 | 23 | export const EditButton = styled.button` 24 | width: 4.6vw; 25 | height: 5vh; 26 | background: ${props => (props.checked ? "#ffffff" : "none")}; 27 | border: ${props => (props.checked ? "1px solid #c0c0c0" : "none")}; 28 | border-bottom: ${props => (props.checked ? "none" : "")}; 29 | color: #999999; 30 | cursor: pointer; 31 | font-size: 1rem; 32 | outline: none; 33 | 34 | @media screen and (max-width: 1400px) { 35 | font-size: 0.9rem; 36 | } 37 | 38 | @media screen and (max-width: 800px) { 39 | font-size: 0.8rem; 40 | } 41 | `; 42 | 43 | export const PreviewButton = styled.button` 44 | width: 5vw; 45 | height: 5vh; 46 | color: #007eff; 47 | background: ${props => (props.checked ? "#ffffff" : "none")}; 48 | border: ${props => (props.checked ? "1px solid #c0c0c0" : "none")}; 49 | border-bottom: ${props => props.checked && "1px solid #ffffff"}; 50 | cursor: pointer; 51 | font-size: 1rem; 52 | outline: none; 53 | 54 | @media screen and (max-width: 1400px) { 55 | font-size: 0.9rem; 56 | } 57 | 58 | @media screen and (max-width: 800px) { 59 | font-size: 0.8rem; 60 | } 61 | `; 62 | 63 | export const WriteBox = styled.div` 64 | background-color: none; 65 | overflow-y: auto; 66 | border: 1px solid #c0c0c0; 67 | width: 100%; 68 | height: auto; 69 | display: flex; 70 | flex-direction: column; 71 | padding: 20px; 72 | `; 73 | 74 | export const RegisterButton = styled.button` 75 | border: none; 76 | outline: none; 77 | color: #ffffff; 78 | background-color: #007eff; 79 | width: 8vw; 80 | height: 5vh; 81 | font-size: 1rem; 82 | margin: 20px 0; 83 | float: right; 84 | cursor: pointer; 85 | `; 86 | -------------------------------------------------------------------------------- /src/components/NoticeWrite/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const WriteOptions = styled.div` 4 | width: 100%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | height: 4.7vh; 9 | `; 10 | 11 | export const WriteBoxContainer = styled.div` 12 | display: flex; 13 | flex-direction: column; 14 | ` 15 | 16 | export const ChangeButtonContainer = styled.div` 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | z-index: 99; 21 | `; 22 | 23 | export const EditButton = styled.button` 24 | width: 4.6vw; 25 | height: 5vh; 26 | background: ${props => (props.checked ? "#ffffff" : "none")}; 27 | border: ${props => (props.checked ? "1px solid #c0c0c0" : "none")}; 28 | border-bottom: ${props => (props.checked ? "none" : "")}; 29 | color: #999999; 30 | cursor: pointer; 31 | font-size: 1rem; 32 | outline: none; 33 | 34 | @media screen and (max-width: 1400px) { 35 | font-size: 0.9rem; 36 | } 37 | 38 | @media screen and (max-width: 800px) { 39 | font-size: 0.8rem; 40 | } 41 | `; 42 | 43 | export const PreviewButton = styled.button` 44 | width: 5vw; 45 | height: 5vh; 46 | color: #007eff; 47 | background: ${props => (props.checked ? "#ffffff" : "none")}; 48 | border: ${props => (props.checked ? "1px solid #c0c0c0" : "none")}; 49 | border-bottom: ${props => props.checked && "1px solid #ffffff"}; 50 | cursor: pointer; 51 | font-size: 1rem; 52 | outline: none; 53 | 54 | @media screen and (max-width: 1400px) { 55 | font-size: 0.9rem; 56 | } 57 | 58 | @media screen and (max-width: 800px) { 59 | font-size: 0.8rem; 60 | } 61 | `; 62 | 63 | export const WriteBox = styled.div` 64 | background-color: none; 65 | overflow-y: auto; 66 | border: 1px solid #c0c0c0; 67 | width: 100%; 68 | height: auto; 69 | display: flex; 70 | flex-direction: column; 71 | padding: 20px; 72 | `; 73 | 74 | export const RegisterButton = styled.button` 75 | border: none; 76 | outline: none; 77 | color: #ffffff; 78 | background-color: #007eff; 79 | width: 8vw; 80 | height: 5vh; 81 | font-size: 1rem; 82 | margin: 20px 0; 83 | float: right; 84 | cursor: pointer; 85 | `; 86 | -------------------------------------------------------------------------------- /src/components/WriteBox/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const WriteOptions = styled.div` 4 | width: 100%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | height: 4.7vh; 9 | `; 10 | 11 | export const WriteBoxContainer = styled.div` 12 | display: flex; 13 | flex-direction: column; 14 | ` 15 | 16 | export const ChangeButtonContainer = styled.div` 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | z-index: 99; 21 | `; 22 | 23 | export const EditButton = styled.button` 24 | width: 4.6vw; 25 | height: 5vh; 26 | background: ${props => (props.checked ? "#ffffff" : "none")}; 27 | border: ${props => (props.checked ? "1px solid #c0c0c0" : "none")}; 28 | border-bottom: ${props => (props.checked ? "none" : "")}; 29 | color: #999999; 30 | cursor: pointer; 31 | font-size: 1rem; 32 | outline: none; 33 | 34 | @media screen and (max-width: 1400px) { 35 | font-size: 0.9rem; 36 | } 37 | 38 | @media screen and (max-width: 800px) { 39 | font-size: 0.8rem; 40 | } 41 | `; 42 | 43 | export const PreviewButton = styled.button` 44 | width: 5vw; 45 | height: 5vh; 46 | color: #007eff; 47 | background: ${props => (props.checked ? "#ffffff" : "none")}; 48 | border: ${props => (props.checked ? "1px solid #c0c0c0" : "none")}; 49 | border-bottom: ${props => props.checked && "1px solid #ffffff"}; 50 | cursor: pointer; 51 | font-size: 1rem; 52 | outline: none; 53 | 54 | @media screen and (max-width: 1400px) { 55 | font-size: 0.9rem; 56 | } 57 | 58 | @media screen and (max-width: 800px) { 59 | font-size: 0.8rem; 60 | } 61 | `; 62 | 63 | export const WriteBox = styled.div` 64 | background-color: none; 65 | overflow-y: auto; 66 | border: 1px solid #c0c0c0; 67 | width: 100%; 68 | height: auto; 69 | display: flex; 70 | flex-direction: column; 71 | padding: 20px; 72 | `; 73 | 74 | export const RegisterButton = styled.button` 75 | border: none; 76 | outline: none; 77 | color: #ffffff; 78 | background-color: #007eff; 79 | width: 8vw; 80 | height: 5vh; 81 | font-size: 1rem; 82 | margin: 20px 0; 83 | margin-left: auto; 84 | cursor: pointer; 85 | `; 86 | -------------------------------------------------------------------------------- /src/components/PageContainer/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link, useParams } from "react-router-dom"; 3 | import * as C from "../../components"; 4 | import * as S from "./style"; 5 | 6 | function PageContainer({ 7 | children, 8 | title, 9 | sort, 10 | hasEditButton, 11 | hasPostButton, 12 | hasHistoryButton, 13 | hasDeleteButton, 14 | url, 15 | onClick, 16 | editUrl, 17 | hasTitle 18 | }) { 19 | const { id } = useParams(); 20 | 21 | return ( 22 | <> 23 | <> 24 | 25 | 26 | 27 | 28 | 29 |
30 | {hasTitle && G무위키:} 31 | {title} 32 |
33 | 34 | <> 35 | {hasEditButton && ( 36 | 37 | 편집 38 | 39 | )} 40 | {hasPostButton && ( 41 | 42 | 추가 43 | 44 | )} 45 | {hasHistoryButton && ( 46 | 47 | 역사 48 | 49 | )} 50 | {hasDeleteButton && ( 51 | 52 | 삭제 53 | 54 | )} 55 | 56 | 57 |
58 | 59 | 분류: 60 | {sort} 61 | 62 |
{children}
63 |
64 | 65 | 66 |
67 | 68 | 69 | ); 70 | } 71 | 72 | export default PageContainer; 73 | -------------------------------------------------------------------------------- /src/assets/School.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function School() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default School; 21 | -------------------------------------------------------------------------------- /src/components/EditWrite/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const WriteOptions = styled.div` 4 | width: 100%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | height: 4.7vh; 9 | `; 10 | 11 | export const WriteBoxContainer = styled.div` 12 | display: flex; 13 | flex-direction: column; 14 | ` 15 | 16 | export const ChangeButtonContainer = styled.div` 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | z-index: 99; 21 | `; 22 | 23 | export const EditButton = styled.button` 24 | width: 4.6vw; 25 | height: 5vh; 26 | background: ${props => (props.checked ? "#ffffff" : "none")}; 27 | border: ${props => (props.checked ? "1px solid #c0c0c0" : "none")}; 28 | border-bottom: ${props => (props.checked ? "none" : "")}; 29 | color: #999999; 30 | cursor: pointer; 31 | font-size: 1rem; 32 | outline: none; 33 | 34 | @media screen and (max-width: 1400px) { 35 | font-size: 0.9rem; 36 | } 37 | 38 | @media screen and (max-width: 800px) { 39 | font-size: 0.8rem; 40 | } 41 | `; 42 | 43 | export const PreviewButton = styled.button` 44 | width: 5vw; 45 | height: 5vh; 46 | color: #007eff; 47 | background: ${props => (props.checked ? "#ffffff" : "none")}; 48 | border: ${props => (props.checked ? "1px solid #c0c0c0" : "none")}; 49 | border-bottom: ${props => props.checked && "1px solid #ffffff"}; 50 | cursor: pointer; 51 | font-size: 1rem; 52 | outline: none; 53 | 54 | @media screen and (max-width: 1400px) { 55 | font-size: 0.9rem; 56 | } 57 | 58 | @media screen and (max-width: 800px) { 59 | font-size: 0.8rem; 60 | } 61 | `; 62 | 63 | export const WriteBox = styled.div` 64 | background-color: none; 65 | overflow-y: auto; 66 | border: 1px solid #c0c0c0; 67 | width: 100%; 68 | height: auto; 69 | display: flex; 70 | flex-direction: column; 71 | padding: 20px; 72 | `; 73 | 74 | export const RegisterButton = styled.button` 75 | border: none; 76 | outline: none; 77 | color: #ffffff; 78 | background-color: #007eff; 79 | width: 8vw; 80 | height: 5vh; 81 | font-size: 1rem; 82 | margin: 20px 0; 83 | float: right; 84 | 85 | 86 | &:hover { 87 | cursor: pointer; 88 | } 89 | `; 90 | -------------------------------------------------------------------------------- /src/apis/TokenManager.js: -------------------------------------------------------------------------------- 1 | import { accessToken, refreshToken, accessExp, refreshExp } from "../lib/token"; 2 | 3 | class TokenManager { 4 | constructor() { 5 | this._accessToken = null; 6 | this._refreshToken = null; 7 | this._accessExp = null; 8 | this._refreshExp = null; 9 | this.initToken(); 10 | } 11 | 12 | validateToken(expiredString, token) { 13 | if (!expiredString || !token) return false; 14 | 15 | return this.calculateMinutes(expiredString, 1) >= new Date(); 16 | } 17 | 18 | calculateMinutes(currentDate, addMinute) { 19 | const expiredAt = currentDate ? new Date(currentDate) : new Date(); 20 | expiredAt.setMinutes(expiredAt.getMinutes() - addMinute); 21 | 22 | return expiredAt; 23 | } 24 | 25 | initToken() { 26 | if (typeof window === "undefined") return; 27 | this._accessToken = localStorage.getItem(accessToken); 28 | this._refreshToken = localStorage.getItem(refreshToken); 29 | this._accessExp = localStorage.getItem(accessExp); 30 | this._refreshExp = localStorage.getItem(refreshExp); 31 | } 32 | 33 | setTokens(tokens) { 34 | this._accessToken = tokens.accessToken; 35 | this._refreshToken = tokens.refreshToken; 36 | this._accessExp = tokens.accessExp; 37 | this._refreshExp = tokens.refreshExp; 38 | 39 | localStorage.setItem(accessToken, tokens.accessToken); 40 | localStorage.setItem(refreshToken, tokens.refreshToken); 41 | localStorage.setItem(accessExp, tokens.accessExp); 42 | localStorage.setItem(refreshExp, tokens.refreshExp); 43 | } 44 | 45 | removeTokens() { 46 | if (typeof window === "undefined") return; 47 | this._accessToken = null; 48 | this._refreshToken = null; 49 | this._accessExp = null; 50 | this._refreshExp = null; 51 | 52 | localStorage.removeItem(accessToken); 53 | localStorage.removeItem(refreshToken); 54 | localStorage.removeItem(accessExp); 55 | localStorage.removeItem(refreshExp); 56 | } 57 | 58 | get accessToken() { 59 | return this._accessToken; 60 | } 61 | 62 | get refreshToken() { 63 | return this._refreshToken; 64 | } 65 | 66 | get accessExp() { 67 | return this._accessExp; 68 | } 69 | 70 | get refreshExp() { 71 | return this._refreshExp; 72 | } 73 | } 74 | 75 | export default TokenManager; 76 | -------------------------------------------------------------------------------- /src/store/reissue.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; 2 | import TokenManager from "../apis/TokenManager"; 3 | import observable from "../lib/Observable"; 4 | import axios from "axios"; 5 | import EnvConfig from "../apis/EnvConfig"; 6 | import { toast } from "react-toastify"; 7 | 8 | export const reissueToken = createAsyncThunk( 9 | "reissue/reissueToken", 10 | async (_, { getState, dispatch }) => { 11 | const tokenManager = new TokenManager(); 12 | const { reissue } = getState(); 13 | 14 | if ( 15 | reissue.isLoading || 16 | tokenManager.calculateMinutes(reissue.refreshDate, 1) >= new Date() 17 | ) { 18 | await new Promise(resolve => { 19 | observable.setObserver(resolve); 20 | }); 21 | return; 22 | } 23 | 24 | dispatch( 25 | setRefreshTiming({ isLoading: true, refreshDate: new Date().toString() }) 26 | ); 27 | const { data } = await axios.patch( 28 | "/auth", 29 | {}, 30 | { 31 | baseURL: EnvConfig.GMUWIKI_SERVER_URL, 32 | withCredentials: true, 33 | headers: { 34 | "Refresh-Token": 35 | tokenManager.refreshToken && `Bearer ${tokenManager.refreshToken}`, 36 | }, 37 | } 38 | ); 39 | observable.notifyAll(); 40 | observable.removeAll(); 41 | 42 | tokenManager.setTokens(data); 43 | return data; 44 | } 45 | ); 46 | 47 | const initialState = { 48 | isLoading: false, 49 | refreshDate: "", 50 | }; 51 | 52 | const reissueSlice = createSlice({ 53 | name: "reissue", 54 | initialState, 55 | reducers: { 56 | setRefreshTiming: (state, { payload }) => { 57 | state = payload; 58 | return state; 59 | }, 60 | }, 61 | extraReducers: builder => { 62 | builder.addCase(reissueToken.fulfilled, state => { 63 | state.isLoading = false; 64 | }); 65 | builder.addCase(reissueToken.rejected, state => { 66 | const tokenManager = new TokenManager(); 67 | tokenManager.removeTokens(); 68 | toast.error("다시 로그인 해주세요."); 69 | 70 | state.isLoading = false; 71 | if (window.location.pathname !== "/") window.location.href = "/"; 72 | }); 73 | }, 74 | }); 75 | 76 | export const { setRefreshTiming } = reissueSlice.actions; 77 | 78 | export default reissueSlice; 79 | -------------------------------------------------------------------------------- /src/pages/Role/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import * as S from "./style"; 3 | import * as C from "../../components"; 4 | import RoleImg1 from "../../imgs/RoleImg1.png"; 5 | import RoleImg2 from "../../imgs/RoleImg2.png"; 6 | import { useFetch } from "../../Hooks"; 7 | import { useNavigate } from "react-router-dom"; 8 | import GetRole from "../../lib/GetRole"; 9 | import { toast } from "react-toastify"; 10 | 11 | export default function Role() { 12 | const navigate = useNavigate(); 13 | const role = GetRole(); 14 | 15 | useEffect(() => { 16 | if (role !== "관리자") { 17 | toast.error("권한이 없습니다."); 18 | navigate("/"); 19 | } 20 | }, [role, navigate]); 21 | 22 | const [formData, setFormData] = useState({}); 23 | const { fetch } = useFetch({ 24 | url: `/role/${formData.role === "admin" ? "grant" : "revoke"}`, 25 | method: "patch", 26 | successMessage: "권한 부여에 성공했습니다.", 27 | errors: { 28 | 404: "유저를 찾을 수 없습니다.", 29 | 400: "권한 부여에 실패했습니다." 30 | } 31 | }); 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 | 이메일 39 | setFormData({ ...formData, email: e.target.value })} 42 | /> 43 | 권한 44 | { 48 | setFormData({ ...formData, role: e.target.value }); 49 | }} 50 | > 51 | 52 | 53 | 54 | 55 | 56 | { 62 | fetch({ email: formData.email }); 63 | }} 64 | > 65 | 부여하기 66 | 67 | 68 | 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/assets/Logout.jsx: -------------------------------------------------------------------------------- 1 | function Logout() { 2 | return ( 3 | 10 | 16 | 20 | 26 | 32 | 33 | ); 34 | } 35 | 36 | export default Logout; 37 | -------------------------------------------------------------------------------- /src/assets/H2Icon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function H2Icon() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default H2Icon; 21 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 22 | 27 | 32 | 37 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | G무위키 51 | 52 | 53 | 54 |
55 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/components/PromotionPage/style.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from "styled-components"; 2 | import PromotionBackground from "../../imgs/promotion.webp"; 3 | 4 | const fadeUp = keyframes` 5 | 0% { 6 | opacity: 0; 7 | transform: translateY(4rem); 8 | } 9 | 10 | 100% { 11 | opacity: 1; 12 | transform: translateY(0); 13 | } 14 | `; 15 | 16 | export const PromotionPageContainer = styled.div` 17 | width: 100vw; 18 | height: 100vh; 19 | background-image: url(${PromotionBackground}); 20 | background-repeat: no-repeat; 21 | background-size: cover; 22 | padding: 100px 70px 50px 100px; 23 | `; 24 | 25 | export const PromotionContentBox = styled.div` 26 | position: relative; 27 | width: 100%; 28 | height: 100%; 29 | display: flex; 30 | align-items: flex-start; 31 | animation: ${fadeUp} 1.2s linear 0s; 32 | 33 | img { 34 | max-width: 100%; 35 | height: auto; 36 | } 37 | 38 | @media screen and (max-width: 1200px) { 39 | img { 40 | width: 70%; 41 | height: auto; 42 | } 43 | } 44 | 45 | @media screen and (max-width: 1300px) { 46 | img { 47 | width: 50%; 48 | height: auto; 49 | } 50 | } 51 | 52 | @media screen and (max-width: 1080px) { 53 | img { 54 | display: none; 55 | } 56 | } 57 | `; 58 | 59 | export const PromotionTextBox = styled.div` 60 | display: flex; 61 | flex-direction: column; 62 | align-items: flex-start; 63 | color: #ffffff; 64 | margin-top: 1.3rem; 65 | margin-right: 8vw; 66 | 67 | p { 68 | font-size: 1rem; 69 | font-weight: 300; 70 | margin-bottom: 25px; 71 | line-height: 25px; 72 | } 73 | `; 74 | 75 | export const PromotionTitleBox = styled.div` 76 | margin-bottom: 25px; 77 | display: flex; 78 | align-items: flex-start; 79 | gap: 16px; 80 | 81 | h1 { 82 | font-size: 2.5rem; 83 | font-weight: bold; 84 | } 85 | `; 86 | 87 | export const GauthLoginButton = styled.button` 88 | width: 170px; 89 | height: 40px; 90 | background-color: #ffffff; 91 | border-radius: 6px; 92 | border: none; 93 | outline: none; 94 | color: #2e80cc; 95 | padding: 16px; 96 | cursor: pointer; 97 | display: flex; 98 | align-items: center; 99 | justify-content: space-between; 100 | 101 | span { 102 | font-size: 0.89rem; 103 | font-weight: 600; 104 | } 105 | `; 106 | 107 | export const PromotionComputerBox = styled.div` 108 | position: absolute; 109 | right: 0; 110 | bottom: 0; 111 | display: flex; 112 | align-items: flex-end; 113 | `; 114 | 115 | export const Img = styled.img` 116 | max-width: 100%; 117 | height: 715px; 118 | `; 119 | -------------------------------------------------------------------------------- /src/components/Header/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Header = styled.header` 4 | width: 100%; 5 | height: 60px; 6 | background-color: #007eff; 7 | display: flex; 8 | align-items: center; 9 | padding: 0 8vw; 10 | display: flex; 11 | justify-content: space-between; 12 | `; 13 | 14 | export const MenuContainer = styled.div` 15 | display: flex; 16 | align-items: center; 17 | svg { 18 | cursor: pointer; 19 | } 20 | `; 21 | 22 | export const InfoContainer = styled.div` 23 | display: flex; 24 | flex-direction: column; 25 | 26 | span { 27 | color: #fff; 28 | font-weight: 700; 29 | margin-left: 20px; 30 | cursor: pointer; 31 | } 32 | `; 33 | 34 | export const Nav = styled.nav` 35 | display: flex; 36 | padding-left: 3vw; 37 | gap: 3vw; 38 | `; 39 | 40 | export const HeaderItem = styled.div` 41 | display: flex; 42 | align-items: center; 43 | cursor: pointer; 44 | svg { 45 | width: 24px; 46 | height: 24px; 47 | } 48 | span { 49 | color: #fff; 50 | font-weight: 700; 51 | margin-left: 8px; 52 | } 53 | `; 54 | 55 | export const SearchContainer = styled.div` 56 | display: flex; 57 | border: 1px solid #c0c0c0; 58 | cursor: pointer; 59 | position: relative; 60 | 61 | @media screen and (max-width: 700px) { 62 | display: none; 63 | } 64 | `; 65 | 66 | export const SearchInput = styled.input` 67 | display: flex; 68 | width: 10vw; 69 | height: 30px; 70 | outline: none; 71 | border: none; 72 | text-indent: 10px; 73 | 74 | &::placeholder { 75 | color: #c0c0c0; 76 | } 77 | `; 78 | 79 | export const SearchIcon = styled.div` 80 | display: flex; 81 | align-items: center; 82 | justify-content: center; 83 | width: 30px; 84 | height: 30px; 85 | background-color: #fff; 86 | border-left: 1px solid #c0c0c0; 87 | 88 | svg { 89 | width: 16px; 90 | height: 16px; 91 | } 92 | `; 93 | 94 | export const SearchItem = styled.ul` 95 | background-color: white; 96 | width: 10.1vw; 97 | height: 30px; 98 | border: 1px solid #c0c0c0; 99 | color: black; 100 | font-size: 12.5px; 101 | border-top: 0; 102 | display: flex; 103 | align-items: center; 104 | text-indent: 9px; 105 | position: absolute; 106 | top: ${prop => prop.top}px; 107 | z-index: 3; 108 | white-space: nowrap; 109 | overflow: hidden; 110 | text-overflow: ellipsis; 111 | 112 | &:hover { 113 | background-color: #e4f1ff; 114 | } 115 | 116 | background-color: ${({ current }) => current && "#e4f1ff"}; 117 | `; 118 | 119 | export const FixedInput = styled.div` 120 | display: flex; 121 | 122 | span { 123 | display: flex; 124 | align-items: center; 125 | } 126 | `; 127 | -------------------------------------------------------------------------------- /src/assets/H3Icon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function H3Icon() { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default H3Icon; 21 | -------------------------------------------------------------------------------- /src/components/EditNotice/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import * as S from "./style"; 3 | import * as C from "../index"; 4 | import { useEdit } from "../../Hooks"; 5 | 6 | const EditNotice = ({ title, content, id }) => { 7 | const [edit, setEdit] = useState(true); 8 | const [preview, setPreview] = useState(false); 9 | const [editContent, setEditContent] = useState(""); 10 | const [editTitle, setEditTitle] = useState(""); 11 | 12 | let save = []; 13 | 14 | const [numArr, setNumArr] = useState([1]); 15 | const textareaRef = useRef(null); 16 | 17 | useEffect(() => { 18 | setEditContent(content); 19 | setEditTitle(title); 20 | 21 | for (let i = 1; i <= content.split("\n").length; i++) { 22 | save.push(i); 23 | } 24 | setNumArr(save); 25 | }, [content, title]); 26 | 27 | useEffect(() => { 28 | function handleUnLoad(e) { 29 | e.returnValue = ""; 30 | e.preventDefault(); 31 | } 32 | window.addEventListener("beforeunload", handleUnLoad); 33 | 34 | return () => { 35 | window.removeEventListener("beforeunload", handleUnLoad); 36 | }; 37 | }, []); 38 | 39 | const onChange = e => { 40 | setEditTitle(e.target.value); 41 | }; 42 | 43 | const onChangeTextArea = e => { 44 | setEditContent(e.target.value); 45 | const textarea = textareaRef.current; 46 | 47 | for (let i = 1; i <= textarea.value.split("\n").length; i++) { 48 | save.push(i); 49 | } 50 | setNumArr(save); 51 | }; 52 | 53 | const handleEdit = () => { 54 | setEdit(true); 55 | setPreview(false); 56 | }; 57 | 58 | const handlePreview = () => { 59 | setEdit(false); 60 | setPreview(true); 61 | }; 62 | 63 | const { editNoticeUpload } = useEdit({ 64 | props: { id, editContent, editTitle } 65 | }); 66 | 67 | const editPost = () => { 68 | const shouldPost = window.confirm("이 공지를 편집하시겠습니까?"); 69 | if (shouldPost) { 70 | editNoticeUpload(); 71 | } 72 | }; 73 | 74 | return ( 75 | <> 76 | 77 | 78 | 79 | 편집 80 | 81 | 82 | 미리보기 83 | 84 | 85 | {edit && ( 86 | 93 | )} 94 | 95 | {edit && ( 96 | 97 | 105 | 106 | )} 107 | {preview && ( 108 | 109 | 110 | 111 | )} 112 | 편집하기 113 | 114 | ); 115 | }; 116 | 117 | export default React.memo(EditNotice); 118 | -------------------------------------------------------------------------------- /src/components/NoticeWrite/index.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useReducer, useRef } from "react"; 2 | import * as S from "./style"; 3 | import * as C from "../index"; 4 | import { useNotice } from "../../Hooks"; 5 | import { useEffect } from "react"; 6 | function reducer(state, action) { 7 | return { 8 | ...state, 9 | [action.name]: action.value 10 | }; 11 | } 12 | 13 | export default function NoticeWrite() { 14 | const [edit, setEdit] = useState(true); 15 | const [preview, setPreview] = useState(false); 16 | const [state, dispatch] = useReducer(reducer, { 17 | title: "" 18 | }); 19 | 20 | let save = []; 21 | 22 | const { title } = state; 23 | const [numArr, setNumArr] = useState([1]); 24 | const [content, setContent] = useState(""); 25 | const textareaRef = useRef(null); 26 | 27 | const onChange = e => { 28 | dispatch(e.target); 29 | }; 30 | 31 | const onChangeTextArea = e => { 32 | setContent(e.target.value); 33 | const textarea = textareaRef.current; 34 | const lineHeight = parseInt(window.getComputedStyle(textarea).lineHeight); 35 | textarea.style.height = `${lineHeight}px`; 36 | const numberOfLines = Math.floor(textarea.scrollHeight / lineHeight); 37 | textarea.style.height = `${numberOfLines * lineHeight + 20}px`; 38 | 39 | setNumArr([]); 40 | for (let i = 1; i <= numberOfLines; i++) { 41 | save.push(i); 42 | } 43 | setNumArr(save); 44 | }; 45 | 46 | const handleEdit = () => { 47 | setEdit(true); 48 | setPreview(false); 49 | }; 50 | 51 | const handlePreview = () => { 52 | setEdit(false); 53 | setPreview(true); 54 | }; 55 | 56 | useEffect(() => { 57 | function handleUnLoad(e) { 58 | e.returnValue = ""; 59 | e.preventDefault(); 60 | } 61 | window.addEventListener("beforeunload", handleUnLoad); 62 | 63 | return () => { 64 | window.removeEventListener("beforeunload", handleUnLoad); 65 | }; 66 | }, []); 67 | 68 | const { uploadNotice } = useNotice({ props: { title, content } }); 69 | 70 | const handleNotice = () => { 71 | const shouldPost = window.confirm("공지를 등록하시겠습니까?"); 72 | if (shouldPost) { 73 | uploadNotice(); 74 | } 75 | }; 76 | 77 | return ( 78 | <> 79 | 80 | 81 | 82 | 편집 83 | 84 | 85 | 미리보기 86 | 87 | 88 | {edit && ( 89 | 96 | )} 97 | 98 | {edit && ( 99 | 100 | 108 | 109 | )} 110 | {preview && ( 111 | 112 | 113 | 114 | )} 115 | 등록하기 116 | 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | # Gmu-WiKi-Front-V2 72 | -------------------------------------------------------------------------------- /src/components/EditWrite/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import * as S from "./style"; 3 | import * as C from "../index"; 4 | import { useEdit } from "../../Hooks"; 5 | 6 | const EditWrite = ({ title, content, id }) => { 7 | const [edit, setEdit] = useState(true); 8 | const [preview, setPreview] = useState(false); 9 | const [editContent, setEditContent] = useState(""); 10 | const [editTitle, setEditTitle] = useState(""); 11 | 12 | let save = []; 13 | 14 | const [numArr, setNumArr] = useState([]); 15 | const textareaRef = useRef(null); 16 | 17 | useEffect(() => { 18 | setEditContent(content); 19 | setEditTitle(title); 20 | 21 | for (let i = 1; i <= content.split("\n").length; i++) { 22 | save.push(i); 23 | } 24 | setNumArr(save); 25 | }, [content, title]); 26 | 27 | useEffect(() => { 28 | function handleUnLoad(e) { 29 | e.returnValue = ""; 30 | e.preventDefault(); 31 | } 32 | window.addEventListener("beforeunload", handleUnLoad); 33 | 34 | return () => { 35 | window.removeEventListener("beforeunload", handleUnLoad); 36 | }; 37 | }, []); 38 | 39 | const onChange = e => { 40 | setEditTitle(e.target.value); 41 | }; 42 | 43 | const onChangeTextArea = e => { 44 | setEditContent(e.target.value); 45 | const textarea = textareaRef.current; 46 | const lineHeight = parseInt(window.getComputedStyle(textarea).lineHeight); 47 | textarea.style.height = `${lineHeight}px`; 48 | const numberOfLines = Math.floor(textarea.scrollHeight / lineHeight); 49 | textarea.style.height = `${numberOfLines * lineHeight + 20}px`; 50 | 51 | setNumArr([]); 52 | for (let i = 1; i <= numberOfLines; i++) { 53 | save.push(i); 54 | } 55 | setNumArr(save); 56 | }; 57 | 58 | const handleEdit = () => { 59 | setEdit(true); 60 | setPreview(false); 61 | }; 62 | 63 | const handlePreview = () => { 64 | setEdit(false); 65 | setPreview(true); 66 | }; 67 | 68 | const { editBoardUpload } = useEdit({ 69 | props: { id, editContent, editTitle } 70 | }); 71 | 72 | const editPost = () => { 73 | const shouldPost = window.confirm("이 글을 편집하시겠습니까?"); 74 | if (shouldPost) { 75 | editBoardUpload(); 76 | } 77 | }; 78 | 79 | return ( 80 | <> 81 | 82 | 83 | 84 | 편집 85 | 86 | 87 | 미리보기 88 | 89 | 90 | {edit && ( 91 | 98 | )} 99 | 100 | {edit && ( 101 | 102 | 110 | 111 | )} 112 | {preview && ( 113 | 114 | 115 | 116 | )} 117 | 편집하기 118 | 119 | ); 120 | }; 121 | 122 | export default React.memo(EditWrite); 123 | -------------------------------------------------------------------------------- /src/components/WriteBox/index.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useReducer, useRef, useState } from "react"; 2 | import * as S from "./style"; 3 | import * as C from "../index"; 4 | import { useUpload } from "../../Hooks"; 5 | 6 | function reducer(state, action) { 7 | return { 8 | ...state, 9 | [action.name]: action.value 10 | }; 11 | } 12 | 13 | function WriteBox() { 14 | const [edit, setEdit] = useState(true); 15 | const [preview, setPreview] = useState(false); 16 | const [state, dispatch] = useReducer(reducer, { 17 | category: "선택해주세요", 18 | detailCategory: "선택해주세요", 19 | title: "" 20 | }); 21 | 22 | let save = []; 23 | 24 | const { category, detailCategory, title } = state; 25 | const [numArr, setNumArr] = useState([1]); 26 | const [content, setContent] = useState(""); 27 | const textareaRef = useRef(null); 28 | 29 | const onChange = e => { 30 | dispatch(e.target); 31 | }; 32 | 33 | useEffect(() => { 34 | function handleUnLoad(e) { 35 | e.returnValue = ""; 36 | e.preventDefault(); 37 | } 38 | window.addEventListener("beforeunload", handleUnLoad); 39 | 40 | return () => { 41 | window.removeEventListener("beforeunload", handleUnLoad); 42 | }; 43 | }, []); 44 | 45 | const onChangeTextArea = e => { 46 | const textarea = textareaRef.current; 47 | const lineHeight = parseInt(window.getComputedStyle(textarea).lineHeight); 48 | textarea.style.height = `${lineHeight}px`; 49 | const numberOfLines = Math.floor(textarea.scrollHeight / lineHeight); 50 | textarea.style.height = `${numberOfLines * lineHeight + 20}px`; 51 | 52 | setNumArr([]); 53 | for (let i = 1; i <= numberOfLines; i++) { 54 | save.push(i); 55 | } 56 | 57 | setNumArr(save); 58 | setContent(e.target.value); 59 | }; 60 | 61 | const handleEdit = () => { 62 | setEdit(true); 63 | setPreview(false); 64 | }; 65 | 66 | const handlePreview = () => { 67 | setEdit(false); 68 | setPreview(true); 69 | }; 70 | 71 | const { uploadHandler } = useUpload({ 72 | props: { title, content, category, detailCategory } 73 | }); 74 | 75 | const post = () => { 76 | const shouldPost = window.confirm("글을 등록하시겠습니까?"); 77 | if (shouldPost) { 78 | uploadHandler(); 79 | } 80 | }; 81 | 82 | return ( 83 | 84 | 85 | 86 | 87 | 편집 88 | 89 | 90 | 미리보기 91 | 92 | 93 | {edit && ( 94 | 101 | )} 102 | 103 | {edit && ( 104 | 105 | 116 | 117 | )} 118 | {preview && ( 119 | 120 | 121 | 122 | )} 123 | 등록하기 124 | 125 | ); 126 | } 127 | 128 | export default WriteBox; 129 | -------------------------------------------------------------------------------- /src/components/InquiryWrite/index.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useReducer, useRef } from "react"; 2 | import * as S from "./style"; 3 | import * as C from "../index"; 4 | import { useInquiry } from "../../Hooks"; 5 | import { useEffect } from "react"; 6 | 7 | function reducer(state, action) { 8 | return { 9 | ...state, 10 | [action.name]: action.value 11 | }; 12 | } 13 | 14 | export default function InquiryWrite() { 15 | const [edit, setEdit] = useState(true); 16 | const [preview, setPreview] = useState(false); 17 | const [state, dispatch] = useReducer(reducer, { 18 | purpose: "선택해주세요", 19 | title: "" 20 | }); 21 | const [showModal, setShowModal] = useState(false); 22 | 23 | let save = []; 24 | 25 | const { title, category } = state; 26 | const [numArr, setNumArr] = useState([1]); 27 | const [content, setContent] = useState(""); 28 | const textareaRef = useRef(null); 29 | 30 | const onChange = e => { 31 | dispatch(e.target); 32 | }; 33 | 34 | const onChangeTextArea = e => { 35 | setContent(e.target.value); 36 | const textarea = textareaRef.current; 37 | const lineHeight = parseInt(window.getComputedStyle(textarea).lineHeight); 38 | textarea.style.height = `${lineHeight}px`; 39 | const numberOfLines = Math.floor(textarea.scrollHeight / lineHeight); 40 | textarea.style.height = `${numberOfLines * lineHeight + 20}px`; 41 | 42 | setNumArr([]); 43 | for (let i = 1; i <= numberOfLines; i++) { 44 | save.push(i); 45 | } 46 | setNumArr(save); 47 | }; 48 | 49 | const handleEdit = () => { 50 | setEdit(true); 51 | setPreview(false); 52 | }; 53 | 54 | const handlePreview = () => { 55 | setEdit(false); 56 | setPreview(true); 57 | }; 58 | 59 | useEffect(() => { 60 | function handleUnLoad(e) { 61 | e.returnValue = ""; 62 | e.preventDefault(); 63 | } 64 | window.addEventListener("beforeunload", handleUnLoad); 65 | 66 | return () => { 67 | window.removeEventListener("beforeunload", handleUnLoad); 68 | }; 69 | }, []); 70 | 71 | const { inquiryUpload } = useInquiry({ props: { title, content, category } }); 72 | 73 | const postInquiry = () => { 74 | const shouldPost = window.confirm("문의를 등록하시겠습니까?"); 75 | 76 | if (shouldPost) { 77 | inquiryUpload(); 78 | setShowModal(true); 79 | } 80 | }; 81 | 82 | return ( 83 | <> 84 | 85 | 86 | 87 | 편집 88 | 89 | 90 | 미리보기 91 | 92 | 93 | {edit && ( 94 | 101 | )} 102 | 103 | {edit && ( 104 | 105 | 115 | 116 | )} 117 | {preview && ( 118 | 119 | 120 | 121 | )} 122 | 등록하기 123 | {showModal && } 124 | 125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /src/pages/Main/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const DetailCenter = styled.div` 4 | height: auto; 5 | width: 100%; 6 | border-bottom: 1px solid #d9d9d9; 7 | padding-bottom: 20px; 8 | 9 | span { 10 | color: #636363; 11 | } 12 | 13 | .pointWord { 14 | color: #007eff; 15 | } 16 | `; 17 | 18 | export const Title = styled.div` 19 | display: flex; 20 | justify-content: center; 21 | font-weight: 600; 22 | font-size: 22px; 23 | margin: 20px 0 10px 0; 24 | 25 | .point { 26 | color: #007eff; 27 | font-weight: 600; 28 | margin-left: 4px; 29 | } 30 | `; 31 | 32 | export const Illusion = styled.div` 33 | display: flex; 34 | justify-content: center; 35 | margin-top: 12px; 36 | font-size: 14px; 37 | `; 38 | 39 | export const Content = styled.div` 40 | margin-top: 20px; 41 | display: flex; 42 | align-items: center; 43 | flex-direction: column; 44 | font-size: 14px; 45 | 46 | .contentCenter { 47 | display: flex; 48 | justify-content: center; 49 | } 50 | 51 | .pointContent { 52 | margin-top: 5px; 53 | } 54 | `; 55 | 56 | export const EmailLink = styled.a` 57 | color: #007eff; 58 | `; 59 | 60 | export const Outline = styled.div` 61 | margin-left: 28px; 62 | `; 63 | 64 | export const OutlineContent = styled.div` 65 | margin-bottom: 30px; 66 | `; 67 | 68 | export const SchoolVideoContainer = styled.div` 69 | position: relative; 70 | padding-top: 56%; 71 | width: 100%; 72 | height: 0; 73 | 74 | iframe { 75 | position: absolute; 76 | top: 0; 77 | left: 50%; 78 | transform: translateX(-50%); 79 | width: 80%; 80 | height: 100%; 81 | display: flex; 82 | justify-content: center; 83 | margin: 0 auto; 84 | } 85 | `; 86 | 87 | export const Lesson = styled.div` 88 | padding: 24px 28px 24px 20px; 89 | background-color: #f1f1f5; 90 | border-left: 8px solid #007eff; 91 | width: 300px; 92 | text-align: center; 93 | `; 94 | 95 | export const SchoolSonContainer = styled.div` 96 | text-align: center; 97 | img { 98 | object-fit: contain; 99 | } 100 | `; 101 | 102 | export const DepartmentContainer = styled.div` 103 | margin-left: 28px; 104 | `; 105 | 106 | export const DepartmentTitle = styled.div` 107 | font-size: 24px; 108 | font-weight: 600; 109 | margin-top: 40px; 110 | `; 111 | 112 | export const DepartmentContent = styled.div` 113 | margin-top: 20px; 114 | `; 115 | 116 | export const IntroCenter = styled.div` 117 | display: flex; 118 | align-items: center; 119 | flex-direction: column; 120 | margin-top: 35px; 121 | `; 122 | 123 | export const SchoolTitleContent = styled.div` 124 | width: 44vw; 125 | height: 80px; 126 | font-weight: 700; 127 | background-color: #007eff; 128 | color: white; 129 | display: flex; 130 | align-items: center; 131 | flex-direction: column; 132 | justify-content: center; 133 | 134 | .englishName { 135 | font-size: 14px; 136 | @media screen and (max-width: 1000px) { 137 | font-size: 0.8; 138 | } 139 | 140 | @media screen and (max-width: 800px) { 141 | font-size: 0.4rem; 142 | } 143 | } 144 | .koreanName { 145 | font-size: 24px; 146 | margin-bottom: 5px; 147 | @media screen and (max-width: 1000px) { 148 | font-size: 1.4rem; 149 | } 150 | @media screen and (max-width: 800px) { 151 | font-size: 0.9rem; 152 | } 153 | } 154 | @media screen and (max-width: 1280px) { 155 | width: 100%; 156 | } 157 | `; 158 | 159 | export const SchoolImg = styled.img` 160 | width: 44vw; 161 | height: 24vw; 162 | @media screen and (max-width: 1280px) { 163 | width: 100%; 164 | height: 100%; 165 | } 166 | `; 167 | -------------------------------------------------------------------------------- /src/lib/mainPageData.js: -------------------------------------------------------------------------------- 1 | export const schoolGraphData = [ 2 | { 3 | titleChild: "개교", 4 | contentChild: "1954년 6월", 5 | backgroundColor: "#DFE464" 6 | }, 7 | { 8 | titleChild: "마이스터 지정", 9 | contentChild: "2017년 3월", 10 | backgroundColor: "#DFE464" 11 | }, 12 | { 13 | titleChild: "성별", 14 | contentChild: "남녀공학", 15 | backgroundColor: "#DFE464" 16 | }, 17 | { 18 | titleChild: "유형", 19 | contentChild: "마이스터고등학교", 20 | backgroundColor: "#DFE464" 21 | }, 22 | { 23 | titleChild: "교장", 24 | contentChild: "최홍진", 25 | backgroundColor: "#DFE464" 26 | }, 27 | { 28 | titleChild: "교감", 29 | contentChild: "김광진", 30 | backgroundColor: "#62ADE0" 31 | }, 32 | { 33 | titleChild: "학생 수", 34 | contentChild: "215명(2023년 기준)", 35 | backgroundColor: "#62ADE0" 36 | }, 37 | { 38 | titleChild: "교훈", 39 | contentChild: "더불어 살아가는 사람다운 미래 인재", 40 | backgroundColor: "#62ADE0" 41 | }, 42 | { 43 | titleChild: "교화", 44 | contentChild: "동백꽃", 45 | backgroundColor: "#62ADE0" 46 | }, 47 | { 48 | titleChild: "교목", 49 | contentChild: "소나무", 50 | backgroundColor: "#62ADE0" 51 | }, 52 | { 53 | titleChild: "교색", 54 | contentChild: "파란색", 55 | backgroundColor: "#123262" 56 | }, 57 | { 58 | titleChild: "교직원 수", 59 | contentChild: "55명(2023년 기준)", 60 | backgroundColor: "#123262" 61 | }, 62 | { 63 | titleChild: "설립형태", 64 | contentChild: "공립", 65 | backgroundColor: "#123262" 66 | }, 67 | { 68 | titleChild: "주소", 69 | contentChild: "광주광역시 광산구 상무대로 312", 70 | backgroundColor: "#123262" 71 | } 72 | ]; 73 | 74 | export const historyGraphData = [ 75 | { 76 | titleChild: "날짜", 77 | contentChild: "내용", 78 | backgroundColor: "#007EFF", 79 | color: "true", 80 | contentColor: "true" 81 | }, 82 | { 83 | titleChild: "1954년 5월 29일", 84 | contentChild: "송정여자상업고등학교 설립 인가", 85 | backgroundColor: "#DFE464" 86 | }, 87 | { 88 | titleChild: "1954년 6월 1일", 89 | contentChild: "전남 광산군 송정읍 785번지에서 송정여자상업고등학교 개교", 90 | backgroundColor: "#DFE464" 91 | }, 92 | { 93 | titleChild: "1979년 3월 1일", 94 | contentChild: "송정여자중학교와 병설 분리", 95 | backgroundColor: "#DFE464" 96 | }, 97 | { 98 | titleChild: "1979년 12월 28일", 99 | contentChild: "학칙변경 상업과 5학급 인가", 100 | backgroundColor: "#DFE464" 101 | }, 102 | { 103 | titleChild: "1996년 3월 1일", 104 | contentChild: "광주여자전산상업고등학교로 교명 변경, 학교 이전", 105 | backgroundColor: "#DFE464" 106 | }, 107 | { 108 | titleChild: "2001년 3월 1일", 109 | contentChild: "광주전산고등학교로 교명 변경", 110 | backgroundColor: "#DFE464" 111 | }, 112 | { 113 | titleChild: "2003년 3월 1일", 114 | contentChild: "실습동 구축", 115 | backgroundColor: "#62ADE0" 116 | }, 117 | { 118 | titleChild: "2005년 12월 31일", 119 | contentChild: "실습동 증축/이전전 ", 120 | backgroundColor: "#62ADE0" 121 | }, 122 | { 123 | titleChild: "2008년 3월 3일", 124 | contentChild: "특수학급 1학급 신설", 125 | backgroundColor: "#62ADE0" 126 | }, 127 | { 128 | titleChild: "2009년 11월 4일", 129 | contentChild: "금봉관 개관", 130 | backgroundColor: "#62ADE0" 131 | }, 132 | { 133 | titleChild: "2014년 3월 1일", 134 | contentChild: "교명 및 학과 개편", 135 | backgroundColor: "#62ADE0" 136 | }, 137 | { 138 | titleChild: "2015년 3월 1일", 139 | contentChild: "광주경영고등학교 제 25대 이윤일 교장 취임", 140 | backgroundColor: "#62ADE0" 141 | }, 142 | { 143 | titleChild: "2015년 10월 1일", 144 | contentChild: "교육부 지정 제 11차 SW분야 마이스터고등학교 지정", 145 | backgroundColor: "#62ADE0" 146 | }, 147 | { 148 | titleChild: "2016년 2월 4일", 149 | contentChild: "제 60회 233명 졸업", 150 | backgroundColor: "#123262" 151 | }, 152 | { 153 | titleChild: "2016년 3월 2일", 154 | contentChild: "2016학년도 신입생 입학", 155 | backgroundColor: "#123262" 156 | }, 157 | { 158 | titleChild: "2016년 9월 1일", 159 | contentChild: "광주소프트웨아마이스터고등학교 제 26대 고익종 교장 취임", 160 | backgroundColor: "#123262" 161 | }, 162 | { 163 | titleChild: "2017년 3월 2일", 164 | contentChild: "마이스터고등학교 전환 및 광주소프트웨어마이스터고로 변경", 165 | backgroundColor: "#123262" 166 | }, 167 | { 168 | titleChild: "2017년 10월 23일", 169 | contentChild: "광주소프트웨어마이스터고등학교 개교식", 170 | backgroundColor: "#123262" 171 | }, 172 | { 173 | titleChild: "2018년 3월 2일", 174 | contentChild: "2018학년도 신입생 입학(총 84명)", 175 | backgroundColor: "#123262" 176 | }, 177 | { 178 | titleChild: "2020년 9월 1일", 179 | contentChild: "광주소프트웨어마이스터고등학교 제 27대 김희철 교장 취임", 180 | backgroundColor: "#123262" 181 | }, 182 | { 183 | titleChild: "2023년 3월 1일", 184 | contentChild: "광주소프트웨어마이스터고등학교 제 28대 최홍진 교장 취임", 185 | backgroundColor: "#123262" 186 | } 187 | ]; 188 | --------------------------------------------------------------------------------