From 4331c5ea49df3abcce1f8b5c66da4454782c2165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=98=EC=A7=84?= Date: Mon, 11 Mar 2024 23:36:52 +0900 Subject: [PATCH] =?UTF-8?q?feat=20::=20presigned=5Furl=20&=20=EA=B3=B5?= =?UTF-8?q?=EC=A7=80=EC=82=AC=ED=95=AD=20=ED=8D=BC=EB=B8=94=EB=A6=AC?= =?UTF-8?q?=EC=8B=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 + src/Apis/File/index.ts | 15 ++ src/Apis/Files/index.ts | 59 +++++++ src/Apis/Files/request.ts | 6 + src/Apis/Files/response.ts | 8 + src/Apis/Notices/index.ts | 21 +++ src/Apis/Notices/request.ts | 5 + src/Components/Header/index.tsx | 3 + src/Pages/NoticePage/NoticeEditPage/index.tsx | 151 ++++++++++++++++++ src/Pages/NoticePage/NoticeEditPage/style.ts | 126 +++++++++++++++ src/Pages/NoticePage/NoticeListPage/index.tsx | 46 ++++++ src/Pages/NoticePage/NoticeListPage/style.ts | 133 +++++++++++++++ .../NoticePage/NoticeWritePage/index.tsx | 141 ++++++++++++++++ src/Pages/NoticePage/NoticeWritePage/style.ts | 126 +++++++++++++++ src/router.tsx | 18 +++ 15 files changed, 861 insertions(+) create mode 100644 src/Apis/Files/index.ts create mode 100644 src/Apis/Files/request.ts create mode 100644 src/Apis/Files/response.ts create mode 100644 src/Apis/Notices/index.ts create mode 100644 src/Apis/Notices/request.ts create mode 100644 src/Pages/NoticePage/NoticeEditPage/index.tsx create mode 100644 src/Pages/NoticePage/NoticeEditPage/style.ts create mode 100644 src/Pages/NoticePage/NoticeListPage/index.tsx create mode 100644 src/Pages/NoticePage/NoticeListPage/style.ts create mode 100644 src/Pages/NoticePage/NoticeWritePage/index.tsx create mode 100644 src/Pages/NoticePage/NoticeWritePage/style.ts diff --git a/package.json b/package.json index 27a851a..52e4d72 100644 --- a/package.json +++ b/package.json @@ -58,5 +58,8 @@ "@types/styled-components": "^5.1.26", "prettier": "2.8.8", "typescript": "4.5" + }, + "compilerOptions": { + "target": "es6" } } diff --git a/src/Apis/File/index.ts b/src/Apis/File/index.ts index dd1153d..5a5bc56 100644 --- a/src/Apis/File/index.ts +++ b/src/Apis/File/index.ts @@ -54,3 +54,18 @@ export const useFileUpload = (file: File, options: MutationOptions) => { } ); }; + + +/** 선생님 모집의뢰 상태 변경 */ +export const useUpload = (files: File[], options: MutationOptions) => { + const formData = new FormData(); + files.forEach((file) => { + formData.append('file', file); + }) + return useMutation( + async () => instance.post(`${router}?type=EXTENSION_FILE`, formData), + { + ...options, + } + ); +}; \ No newline at end of file diff --git a/src/Apis/Files/index.ts b/src/Apis/Files/index.ts new file mode 100644 index 0000000..cffd77a --- /dev/null +++ b/src/Apis/Files/index.ts @@ -0,0 +1,59 @@ +import { useMutation } from "react-query" +import { PresignedUrlRequest } from "./request" +import { PresignedUrlResponse } from "./response" +import { instance } from "../axios" +import axios from "axios" + +// export const usePresignedUrl = () => { +// return useMutation( +// async (attachments: File[]) => { +// const files = attachments.map((item) => ({ +// type: 'EXTENSION_FILE', +// file_name: item.name, +// })); + +// const data = await instance.post(`${process.env.REACT_APP_BASE_URL}/files/pre-signed`, {files}); + +// return data; +// }, { +// onSuccess: ({data}) => { +// console.log(data); +// } +// } +// ) +// } +// propsData: PresignedUrlRequest/ + +export const usePresignedUrl = () => { + return useMutation( + async (attachments: File[]) => { + const files = attachments.map((item) => ({ + type: 'EXTENSION_FILE', + file_name: item.name, + })); + + const { data: presignedUrls } = await instance.post(`${process.env.REACT_APP_BASE_URL}/files/pre-signed`, { files }); + return {presignedUrls, attachments}; + }, { + onSuccess: async ({ presignedUrls, attachments }) => { + + const uploadPromises = presignedUrls.urls.map(({pre_signed_url}, idx) => { + (async () => await axios.put(pre_signed_url, attachments[idx]))(); + }); + await Promise.all(uploadPromises); + } + } + ) +} + +// const readFileAsBinaryString = (file: File): Promise => { +// return new Promise((resolve, reject) => { +// const reader = new FileReader(); +// reader.onload = () => { +// const result = reader.result as string; +// resolve(result); +// }; +// reader.onerror = reject; +// reader.readAsBinaryString(file); +// }); +// } diff --git a/src/Apis/Files/request.ts b/src/Apis/Files/request.ts new file mode 100644 index 0000000..becdb28 --- /dev/null +++ b/src/Apis/Files/request.ts @@ -0,0 +1,6 @@ +export interface PresignedUrlRequest { + files: { + type: string + file_name: string + }[] +} \ No newline at end of file diff --git a/src/Apis/Files/response.ts b/src/Apis/Files/response.ts new file mode 100644 index 0000000..d090809 --- /dev/null +++ b/src/Apis/Files/response.ts @@ -0,0 +1,8 @@ +export interface PresignedUrlResponse { + urls: [ + { + file_path: string, + pre_signed_url: string + } + ] +} \ No newline at end of file diff --git a/src/Apis/Notices/index.ts b/src/Apis/Notices/index.ts new file mode 100644 index 0000000..63ab259 --- /dev/null +++ b/src/Apis/Notices/index.ts @@ -0,0 +1,21 @@ +import { NoticeWrite } from "./request"; +import { useMutation } from "react-query"; +import { instance } from "../axios"; + +export const useNoticeWriteData = (noticeData: NoticeWrite) => { + const formData = new FormData() + noticeData.attachments.forEach((attachment) => { formData.append('file', attachment) }) + + return useMutation( + async () => { + const { data } = await instance.post(`${process.env.REACT_APP_BASE_URL}/notices`, {...noticeData, attachments: formData}); + return data; + }, + { + onError: (error: Error) => { + console.error("공지 작성에 실패하였습니다:", error.message); + } + } + ) +} + diff --git a/src/Apis/Notices/request.ts b/src/Apis/Notices/request.ts new file mode 100644 index 0000000..f720010 --- /dev/null +++ b/src/Apis/Notices/request.ts @@ -0,0 +1,5 @@ +export interface NoticeWrite { + title: string; + content: string; + attachments: string[]; +} \ No newline at end of file diff --git a/src/Components/Header/index.tsx b/src/Components/Header/index.tsx index f1c3214..ac2afd3 100644 --- a/src/Components/Header/index.tsx +++ b/src/Components/Header/index.tsx @@ -45,6 +45,9 @@ export function Header() { 지원서 + + <_.NavBtn clicked={clickedStatus('Notice')} width={68}> + 공지 <_.NavBtn clicked={clickedStatus('Banner')} width={68}> 배너 diff --git a/src/Pages/NoticePage/NoticeEditPage/index.tsx b/src/Pages/NoticePage/NoticeEditPage/index.tsx new file mode 100644 index 0000000..d76fdb4 --- /dev/null +++ b/src/Pages/NoticePage/NoticeEditPage/index.tsx @@ -0,0 +1,151 @@ +import { Header } from '../../../Components/Header'; +import * as _ from './style'; +import { ChangeEvent, useEffect, useRef, useState } from 'react'; +import { Button } from '@team-return/design-system'; +import { useNoticeWriteData } from '../../../Apis/Notices'; +import axios from 'axios'; +import { usePresignedUrl } from '../../../Apis/Files'; +import { PresignedUrlRequest } from '../../../Apis/Files/request'; +import { Link } from 'react-router-dom'; + +export function NoticeEditPage() { + const fileInputRef = useRef(null); + const [inputCount, setInputCount] = useState(0); + const [title, setTitle] = useState(''); + const [content, setContent] = useState(''); + const [attachments, setAttachments] = useState([]); + const [presignedUrls, setPresignedUrls] = useState([]); + + const { mutate: writeNotice } = useNoticeWriteData({ + title, + content, + attachments: presignedUrls, + }); + + // const fileName = jsonData.files[0].file_name; + + const { mutate: getPresignedUrl } = usePresignedUrl(); + // const { mutate } = useUpload(attachments, { onSuccess: (e) => {} }); + + const getTodayDate = (): string => { + const today = new Date(); + const year = today.getFullYear(); + let month = (today.getMonth() + 1).toString(); + let day = today.getDate().toString(); + + if (month.length === 1) { + month = '0' + month; + } + if (day.length === 1) { + day = '0' + day; + } + + return `${year}-${month}-${day}`; + }; + + const handleAddFileClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const onInputHandler = (e: any) => { + setInputCount(e.target.value.length); + }; + + const handleTitleChange = (e: ChangeEvent) => { + setTitle(e.target.value); + console.log('제목:', e.target.value); + }; + + const handleContentChange = (e: ChangeEvent) => { + setContent(e.target.value); + console.log('내용:', e.target.value); + }; + + let files_: any; + + const handleFileChange = async (e: ChangeEvent) => { + if (e.target.files) { + const newAttachments = Array.from(e.target.files); + setAttachments((prevAttachments) => [ + ...prevAttachments, + ...newAttachments, + ]); + + files_ = attachments.map((item) => ({ + type: 'EXTENSION_FILE', + file_name: item.name, + })); + + // const presignedUrls = await Promise.all(promises); + + // // setPresignedUrls((prevPresignedUrls) => [ + // // ...prevPresignedUrls, + // // ...presignedUrls, + // // ]); + + // console.log('첨부파일: ', attachments, newAttachments); + } + }; + + const handleNoticeSubmit = () => { + // console.log(data); + // writeNotice(); + getPresignedUrl(attachments); + }; + + return ( + <> +
+ <_.Wrapper> + <_.Box> + <_.Title>공지 작성하기 + <_.ContentWrap> + <_.WriteDateWrap> + <_.Text>작성일 + <_.Text>{getTodayDate()} + + <_.TitleWrap> + <_.Text>제목 + <_.Input placeholder="공지 제목을 입력하세요" /> + + <_.TextWrap> + <_.Text>내용 + <_.InputWrapper> + <_.TextInput + placeholder="공지 내용을 입력하세요" + maxLength={999} + value={content} + onChange={handleContentChange} + onInput={onInputHandler} + /> + <_.InputCount>{inputCount}자/1000 + + + <_.FileWrap> + <_.Text>첨부파일 + <_.AddFile onClick={handleAddFileClick}> + 파일 추가하기 + + + + <_.ButtonWrap> + + + + + + + + + ); +} diff --git a/src/Pages/NoticePage/NoticeEditPage/style.ts b/src/Pages/NoticePage/NoticeEditPage/style.ts new file mode 100644 index 0000000..23fbf65 --- /dev/null +++ b/src/Pages/NoticePage/NoticeEditPage/style.ts @@ -0,0 +1,126 @@ +import styled from 'styled-components'; + +export const Wrapper = styled.div` + width: 100vw; + min-width: 1400px; + background: #fafafa; + display: flex; + flex-direction: column; + align-items: center; + padding-top: 95px; + padding-bottom: 11px; + overflow: scroll; +`; + +export const InputCount = styled.div` + font-size: 16px; + font-weight: 500; + margin-left: auto; +`; + +export const Box = styled.div` + width: 980px; + height: 850px; + border: 1px solid #E5E5E5; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: white; +`; + +export const Title = styled.div` + font-weight: 600; + font-size: 23px; + line-height: 36px; + margin-left: 24px; + align-self: flex-start; + margin-bottom: 25px; +`; + +export const ContentWrap = styled.div` + display: flex; + flex-direction: column; + gap: 30px; + justify-content: center; + align-items: flex-start; + text-align: left; +`; + +export const WriteDateWrap = styled.div` + display: flex; + flex-direction: row; + gap: 56px; + align-items: center; +`; + +export const Text = styled.div` + font-size: 23px; + font-weight: 600; +`; + +export const TitleWrap = styled.div` + display: flex; + flex-direction: row; + gap: 80px; + align-items: center; +`; + +export const Input = styled.input` + width: 545px; + height: 43px; + border: 1px solid #999999; + padding-left: 10px; + font-size: 16px; + border-radius: 4px; +`; + +export const TextWrap = styled.div` + display: flex; + flex-direction: row; + gap: 80px; +`; + +export const InputWrapper = styled.div` + width: 545px; + height: 400px; + border: 1px solid #999999; + border-radius: 4px; + padding: 10px; + overflow: auto; + background-color: white; + display: flex; + flex-direction: column; +`; + +export const TextInput = styled.textarea` + width: 100%; + height: 90%; + font-size: 16px; + border: none; + resize: none; +`; + +export const FileWrap = styled.div` + display: flex; + flex-direction: row; + gap: 37px; + align-items: center; +`; + +export const AddFile = styled.div` + width: 550px; + height: 43px; + background-color: #FAFAFA; + cursor: pointer; + font-size: 16px; + align-items: center; + display: flex; + padding-left: 16px; + font-weight: 400; +`; + +export const ButtonWrap = styled.div` + margin-top: 68px; + margin-left: auto; +`; diff --git a/src/Pages/NoticePage/NoticeListPage/index.tsx b/src/Pages/NoticePage/NoticeListPage/index.tsx new file mode 100644 index 0000000..bbf3bd7 --- /dev/null +++ b/src/Pages/NoticePage/NoticeListPage/index.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import * as _ from './style'; +import { Header } from '../../../Components/Header'; +import { Link } from 'react-router-dom'; +import { Button } from '@team-return/design-system'; + +export function NoticeListPage() { + return ( + <> +
+ <_.Wrapper> + <_.Background> + <_.TitleWrapper> + <_.Title>공지사항 + + + + + <_.Box> + <_.Table> + <_.Thead> + + <_.HeaderNumber>번호 + <_.HeaderTitle>제목 + <_.HeaderDate>작성일 + + + + <_.Tbody> + <_.Tr> + <_.NoticeNumber>12 + <_.NoticeTitle> + [중요] 오리엔테이션날 일정 안내 + + <_.NoticeDate>2024-01-16 + + + + + <_.Bottom> + + + + + ); +} diff --git a/src/Pages/NoticePage/NoticeListPage/style.ts b/src/Pages/NoticePage/NoticeListPage/style.ts new file mode 100644 index 0000000..eceaed9 --- /dev/null +++ b/src/Pages/NoticePage/NoticeListPage/style.ts @@ -0,0 +1,133 @@ +import styled from "styled-components"; + +export const Wrapper = styled.div` + width: 100vw; + min-width: 1400px; + background: #fafafa; + display: flex; + flex-direction: column; + align-items: center; + padding-top: 95px; + padding-bottom: 11px; + overflow: scroll; +`; + +export const Box = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-top: 48px; + gap: 40px; +`; + +export const Table = styled.table` + display: flex; + table-layout: fixed; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +export const Thead = styled.thead` + display: flex; + justify-content: center; + align-items: center; + border-top: 2px solid #7F7F7F; + width: 1139px; + height: 70px; + background-color: #F7F7F7; + border-bottom: 0.5px solid #7F7F7F; +`; + +export const HeaderNumber = styled.th` + width: 192px; + font-size: 16px; + padding: 2px 4px 2px 4px; +`; + +export const HeaderTitle = styled.th` + width: 755px; + font-size: 16px; + padding: 2px 4px 2px 4px; +`; + +export const HeaderDate = styled.th` + width: 192px; + font-size: 16px; + padding: 2px 4px 2px 4px; +`; + +export const Tbody = styled.tbody` + display: flex; + justify-content: center; + align-items: center; + border-collapse: collapse; +`; + +export const Tr = styled.tr` + height: 70px; + border-bottom: 0.5px solid #7F7F7F; + display: flex; + flex-direction: row; + align-items: center; +`; + +export const NoticeNumber = styled.td` + border: none; + width: 192px; + font-size: 16px; + color: #002C53; + display: flex; + justify-content: center; + font-weight: 500; +`; + +export const NoticeTitle = styled.td` + border: none; + width: 755px; + color: 16px; + display: flex; + justify-content: center; + font-weight: 500; +`; + +export const NoticeDate = styled.td` + border: none; + width: 192px; + color: 16px; + display: flex; + justify-content: center; + font-weight: 500; +`; + +export const Bottom = styled.div` + margin-bottom: 98px; +`; + +export const Background = styled.div` + width: 1298px; + height: 971px; + background-color: white; + border: 1px solid #E5E5E5; + display: flex; + flex-direction: column; + align-items: center; + overflow: scroll; +`; + +export const TitleWrapper = styled.div` + display: flex; + margin-top: 24px; +`; + +export const Title = styled.div` + font-size: 40px; + color: #333333; + font-weight: 700; + margin-right: 800px; +`; + +export const Button = styled.button` + padding: 8px 49px; +`; \ No newline at end of file diff --git a/src/Pages/NoticePage/NoticeWritePage/index.tsx b/src/Pages/NoticePage/NoticeWritePage/index.tsx new file mode 100644 index 0000000..b626102 --- /dev/null +++ b/src/Pages/NoticePage/NoticeWritePage/index.tsx @@ -0,0 +1,141 @@ +import { Header } from '../../../Components/Header'; +import * as _ from './style'; +import { ChangeEvent, useEffect, useRef, useState } from 'react'; +import { Button } from '@team-return/design-system'; +import { useNoticeWriteData } from '../../../Apis/Notices'; +import axios from 'axios'; +import { usePresignedUrl } from '../../../Apis/Files'; +import { PresignedUrlRequest } from '../../../Apis/Files/request'; +import { Link } from 'react-router-dom'; + +export function NoticeWritePage() { + const fileInputRef = useRef(null); + const [inputCount, setInputCount] = useState(0); + const [title, setTitle] = useState(''); + const [content, setContent] = useState(''); + const [attachments, setAttachments] = useState([]); + const [presignedUrls, setPresignedUrls] = useState([]); + + const { mutate: writeNotice } = useNoticeWriteData({ + title, + content, + attachments: presignedUrls, + }); + + // const fileName = jsonData.files[0].file_name; + + const { mutate: getPresignedUrl } = usePresignedUrl(); + // const { mutate } = useUpload(attachments, { onSuccess: (e) => {} }); + + const getTodayDate = (): string => { + const today = new Date(); + const year = today.getFullYear(); + let month = (today.getMonth() + 1).toString(); + let day = today.getDate().toString(); + + if (month.length === 1) { + month = '0' + month; + } + if (day.length === 1) { + day = '0' + day; + } + + return `${year}-${month}-${day}`; + }; + + const handleAddFileClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const onInputHandler = (e: any) => { + setInputCount(e.target.value.length); + }; + + const handleTitleChange = (e: ChangeEvent) => { + setTitle(e.target.value); + console.log('제목:', e.target.value); + }; + + const handleContentChange = (e: ChangeEvent) => { + setContent(e.target.value); + console.log('내용:', e.target.value); + }; + + // let files_: any; + + const handleFileChange = async (e: ChangeEvent) => { + if (e.target.files) { + const newAttachments = Array.from(e.target.files); + setAttachments((prevAttachments) => [ + ...prevAttachments, + ...newAttachments, + ]); + } + }; + + const handleNoticeSubmit = () => { + // console.log(data); + // writeNotice(); + getPresignedUrl(attachments); + }; + + return ( + <> +
+ <_.Wrapper> + <_.Box> + <_.Title>공지 작성하기 + <_.ContentWrap> + <_.WriteDateWrap> + <_.Text>작성일 + <_.Text>{getTodayDate()} + + <_.TitleWrap> + <_.Text>제목 + <_.Input + placeholder="공지 제목을 입력하세요" + value={title} + onChange={handleTitleChange} + /> + + <_.TextWrap> + <_.Text>내용 + <_.InputWrapper> + <_.TextInput + placeholder="공지 내용을 입력하세요" + maxLength={999} + value={content} + onChange={handleContentChange} + onInput={onInputHandler} + /> + <_.InputCount>{inputCount}자/1000 + + + <_.FileWrap> + <_.Text>첨부파일 + <_.AddFile onClick={handleAddFileClick}> + 파일 추가하기 + + + + <_.ButtonWrap> + + + + + + + + + ); +} diff --git a/src/Pages/NoticePage/NoticeWritePage/style.ts b/src/Pages/NoticePage/NoticeWritePage/style.ts new file mode 100644 index 0000000..23fbf65 --- /dev/null +++ b/src/Pages/NoticePage/NoticeWritePage/style.ts @@ -0,0 +1,126 @@ +import styled from 'styled-components'; + +export const Wrapper = styled.div` + width: 100vw; + min-width: 1400px; + background: #fafafa; + display: flex; + flex-direction: column; + align-items: center; + padding-top: 95px; + padding-bottom: 11px; + overflow: scroll; +`; + +export const InputCount = styled.div` + font-size: 16px; + font-weight: 500; + margin-left: auto; +`; + +export const Box = styled.div` + width: 980px; + height: 850px; + border: 1px solid #E5E5E5; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: white; +`; + +export const Title = styled.div` + font-weight: 600; + font-size: 23px; + line-height: 36px; + margin-left: 24px; + align-self: flex-start; + margin-bottom: 25px; +`; + +export const ContentWrap = styled.div` + display: flex; + flex-direction: column; + gap: 30px; + justify-content: center; + align-items: flex-start; + text-align: left; +`; + +export const WriteDateWrap = styled.div` + display: flex; + flex-direction: row; + gap: 56px; + align-items: center; +`; + +export const Text = styled.div` + font-size: 23px; + font-weight: 600; +`; + +export const TitleWrap = styled.div` + display: flex; + flex-direction: row; + gap: 80px; + align-items: center; +`; + +export const Input = styled.input` + width: 545px; + height: 43px; + border: 1px solid #999999; + padding-left: 10px; + font-size: 16px; + border-radius: 4px; +`; + +export const TextWrap = styled.div` + display: flex; + flex-direction: row; + gap: 80px; +`; + +export const InputWrapper = styled.div` + width: 545px; + height: 400px; + border: 1px solid #999999; + border-radius: 4px; + padding: 10px; + overflow: auto; + background-color: white; + display: flex; + flex-direction: column; +`; + +export const TextInput = styled.textarea` + width: 100%; + height: 90%; + font-size: 16px; + border: none; + resize: none; +`; + +export const FileWrap = styled.div` + display: flex; + flex-direction: row; + gap: 37px; + align-items: center; +`; + +export const AddFile = styled.div` + width: 550px; + height: 43px; + background-color: #FAFAFA; + cursor: pointer; + font-size: 16px; + align-items: center; + display: flex; + padding-left: 16px; + font-weight: 400; +`; + +export const ButtonWrap = styled.div` + margin-top: 68px; + margin-left: auto; +`; diff --git a/src/router.tsx b/src/router.tsx index 9261d66..aff0b73 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -9,6 +9,10 @@ import { ApplicationViewPage } from './Pages/ApplicationViewPage'; import { StudentManagementPage } from './Pages/StudentManagementPage'; import { CompanyDetailPage } from './Pages/CompanyDetailPage'; import { RecruitmentFormDetailPage } from './Pages/RecruitmentFormDetailPage'; +import { NoticeListPage } from './Pages/NoticePage/NoticeListPage'; +import { NoticeDetailPage } from './Pages/NoticePage/NoticeDetailPage'; +import { NoticeWritePage } from './Pages/NoticePage/NoticeWritePage'; +import { NoticeEditPage } from './Pages/NoticePage/NoticeEditPage'; import { BannerPage } from './Pages/BannerPage'; import { CreateBannerPage } from './Pages/CreateBannerPage'; @@ -61,9 +65,23 @@ const Router = createBrowserRouter([ element: , }, { + path: 'Notice', + element: , path: 'Banner', element: , }, + { + path: 'Notice/Detail', + element: , + }, + { + path: 'Notice/Write', + element: , + }, + { + path: 'Notice/Edit', + element: , + }, { path: 'CreateBanner', element: ,