diff --git a/src/App.tsx b/src/App.tsx index f6053e9..2ec9ece 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -46,7 +46,7 @@ const RouterPath = [ element: , }, { - paht: "QnA", + path: "QnA", element: , }, { @@ -65,5 +65,4 @@ const router = createBrowserRouter([ function App() { return ; } - export default App; diff --git a/src/assets/background.svg b/src/assets/background.svg new file mode 100644 index 0000000..ef32578 --- /dev/null +++ b/src/assets/background.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/copyright.svg b/src/assets/copyright.svg new file mode 100644 index 0000000..ee61103 --- /dev/null +++ b/src/assets/copyright.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/downbtn.svg b/src/assets/downbtn.svg new file mode 100644 index 0000000..37638e2 --- /dev/null +++ b/src/assets/downbtn.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/search.svg b/src/assets/search.svg new file mode 100644 index 0000000..77f123c --- /dev/null +++ b/src/assets/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/pages/Notice/index.tsx b/src/pages/Notice/index.tsx index e2ac67b..c5c5d4a 100644 --- a/src/pages/Notice/index.tsx +++ b/src/pages/Notice/index.tsx @@ -1,7 +1,227 @@ -export default function Notice() { +import React, { useState } from "react"; +import { + container, + titleText, + mid, + notice, + noticeList, + noticeContentWrapper, + noticeContent, + noticeContainer, + searchbar, + searchContainer, + copyright, + noticeNumber, + noticeDownBtn, + noticeDetail, +} from "./notice.css.ts"; // 스타일 가져오기 + +// NoticeItem 타입 정의: 제목은 string, 콘텐츠는 JSX.Element로 지정 +interface NoticeItem { + id: number; + title: string; + content: JSX.Element; + detail: JSX.Element; +} + +// 임시 데이터 예시 +const noticeData: NoticeItem[] = [ + { + id: 1, + title: "2024 강원대학교 백령대동제 개최 안내", + content:

This 강원대 the content for notice 1.

, + detail:

여기는 마지막 내용입니다. Notice 1에 대한 정보가 더 있습니다.

, + }, + { + id: 2, + title: "2024 강원대학교 백령대동제 개최 안내", + content:

This is the content for notice 2.

, + detail:

여기는 룰루 내용입니다.

, + }, + { + id: 3, + title: "랄랄라 신규 게시물", + content:

This is the content for notice 2.

, + detail:

여기는 상세 내용입니다. Notice 2에 대한 정보가 더 있습니다잇.

, + }, + { + id: 4, + title: "2024 강원대학교 백령대동제 개최 안내", + content:

This is the content for notice 2.

, + detail:

여기는 룰루 내용입니다.

, + }, + { + id: 5, + title: "랄랄라 신규 게시물", + content:

This is the content for notice 2.

, + detail:

여기는 상세 내용입니다. Notice 2에 대한 정보가 더 있습니다잇.

, + }, + { + id: 6, + title: "2024 강원대학교 백령대동제 개최 안내", + content:

This is the content for notice 2.

, + detail:

여기는 룰루 내용입니다.

, + }, + { + id: 7, + title: "랄랄라 신규 게시물", + content:

This is the content for notice 2.

, + detail:

여기는 상세 내용입니다. Notice 2에 대한 정보가 더 있습니다잇.

, + }, + { + id: 8, + title: "2024 강원대학교 백령대동제 개최 안내", + content:

This is the content for notice 2.

, + detail:

여기는 룰루 내용입니다.

, + }, + { + id: 9, + title: "랄랄라 신규 게시물", + content:

This is the content for notice 2.

, + detail:

여기는 상세 내용입니다. Notice 2에 대한 정보가 더 있습니다잇.

, + }, + // 나머지 공지사항 추가 +]; + +function Notice() { + const [query, setQuery] = useState(""); // 검색어의 타입을 string으로 지정 + const [currentPage, setCurrentPage] = useState(1); // 현재 페이지 상태 + const [expandedNotices, setExpandedNotices] = useState([]); // 확장된 공지 ID들을 관리 + const itemsPerPage = 6; + + // 가장 최근 게시물의 ID 찾기 (ID가 제일 큰 것이 최신 게시물이라고 가정) + const mostRecentNoticeId = Math.max(...noticeData.map((notice) => notice.id)); + + // 검색어 입력 시 상태 업데이트 + const handleSearch = (event: React.ChangeEvent) => { + setQuery(event.target.value); + setCurrentPage(1); + }; + + // downbtn 클릭 시 상세 내용을 보여주는 로직 + const toggleNotice = (id: number) => { + setExpandedNotices((prev) => + prev.includes(id) ? prev.filter((noticeId) => noticeId !== id) : [...prev, id], + ); + }; + + // 필터링 로직 + const filteredNotices = noticeData + .filter((noticeItem) => { + const searchText = query.toLowerCase(); + return ( + noticeItem.title.toLowerCase().includes(searchText) || + noticeItem.detail.props.children.toString().toLowerCase().includes(searchText) + ); + }) + .reverse(); // 공지사항을 내림차순으로 정렬 (마지막 공지사항이 제일 위로 오게) + + // 현재 페이지에 맞는 데이터 추출 + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentNotices = filteredNotices.slice(indexOfFirstItem, indexOfLastItem); + + // 페이지 수 계산 + const totalPages = Math.ceil(filteredNotices.length / itemsPerPage); + + // 페이지 변경 함수 + const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + return (
-

Notice

+
+

공지사항

+
+
+
+ +
+ + {/* noticeList 렌더링 */} +
+ {filteredNotices.length === 0 ? ( +

+ 해당하는 게시글이 없어요! +

+ ) : ( + currentNotices.map((noticeItem, index) => ( +
+
+ {/* 번호와 제목 표시 */} +
+

{index + 1 + (currentPage - 1) * itemsPerPage}

+ {/* 가장 최근 게시물에만 'New' 표시 */} + {noticeItem.id === mostRecentNoticeId && ( + + New + + )} +
+ +
+

{noticeItem.title}

+
+ {/* downbtn을 클릭하면 토글 */} + +
+ + {/* 상세 내용 표시 - 확장 시만 보여줌 */} + {expandedNotices.includes(noticeItem.id) && ( +
{noticeItem.detail}
+ )} +
+ )) + )} +
+ + {/* 페이지네이션 */} + {filteredNotices.length > 0 && ( +
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNumber) => ( + + ))} +
+ )} +
+ +
+
+
); } + +export default Notice; diff --git a/src/pages/Notice/notice.css.ts b/src/pages/Notice/notice.css.ts new file mode 100644 index 0000000..4ce0b9d --- /dev/null +++ b/src/pages/Notice/notice.css.ts @@ -0,0 +1,165 @@ +import { style, globalStyle } from "@vanilla-extract/css"; + +// body에 글로벌 스타일 적용 +globalStyle("body", { + position: "relative", + margin: 0, + padding: 0, + backgroundImage: 'url("src/assets/background.svg")', // 배경 이미지 경로 + backgroundSize: "cover", // 배경 이미지를 화면 전체에 맞춤 + backgroundPosition: "center", // 이미지를 화면 중앙에 위치 + backgroundRepeat: "no-repeat", // 이미지를 반복하지 않도록 설정 + width: "100%", + height: "100vh", // 전체 화면 높이 적용 + overflow: "hidden", // 스크롤바가 나타나지 않도록 설정 + fontFamily: `'Pretendard', sans-serif`, +}); + +globalStyle("::-webkit-scrollbar", { + width: "0px", + height: "0px", +}); + +export const titleText = style({ + fontSize: 20, + fontWeight: 600, + color: "white", + marginTop: 60, +}); +// 검색 필드와 버튼을 감싸는 컨테이너 +export const searchContainer = style({ + display: "flex", + alignItems: "center", + backgroundColor: "white", + borderRadius: "50px", + boxShadow: "0 5px 5px rgba(0,0,0,0.25)", + width: "80%", + height: "40px", + position: "fixed", + top: 150, + marginBottom: 20, +}); + +// 검색 입력 필드 스타일 +export const searchbar = style({ + width: "100%", // 너비 조정 + height: "100%", + border: "none", + outline: "none", + borderRadius: 50, // 왼쪽만 둥글게 + padding: "0px 15px", + fontFamily: `'Pretendard', sans-serif`, + fontSize: "18px", + fontWeight: 500, +}); + +// 상단의 빨간 컨테이너 +export const container = style({ + width: "100%", + height: 110, + display: "flex", + justifyContent: "center", +}); + +// 중간에 검색창을 배치하는 컨테이너 +export const mid = style({ + position: "relative", + width: "100%", + height: 650, + display: "flex", + margin: "20px 0", + justifyContent: "center", + flexDirection: "column", + alignItems: "center", +}); + +export const noticeList = style({ + width: "100%", + height: 455, + overflowY: "scroll", + display: "flex", + alignItems: "center", + flexDirection: "column", +}); + +export const noticeContainer = style({ + width: "95%", + height: "auto", + borderBottom: "1px solid white", + display: "flex", + justifyContent: "center", + alignItems: "center", + flexDirection: "column", + marginBottom: 7, +}); + +export const notice = style({ + width: "95%", + height: 60, + backgroundColor: "white", + borderRadius: 10, + marginBottom: 7, + display: "flex", + alignItems: "center", + flexDirection: "row", + position: "relative", +}); + +export const copyright = style({ + position: "absolute", + bottom: 0, + margin: 0, + padding: 0, + backgroundImage: 'url("src/assets/copyright.svg")', // 배경 이미지 경로 + backgroundPosition: "center", // 이미지를 화면 중앙에 위치 + backgroundRepeat: "no-repeat", // 이미지를 반복하지 않도록 설정 + width: "90%", + height: 110, // 전체 화면 높이 적용 + overflow: "hidden", // 스크롤바가 나타나지 않도록 설정 +}); + +export const noticeNumber = style({ + fontSize: 25, + fontWeight: 700, + color: "#0081C9", + marginLeft: 20, +}); + +export const noticeContentWrapper = style({ + width: "90%", // 부모 요소의 너비에 맞추기 + overflow: "hidden", // 넘치는 부분을 숨김 + position: "relative", + marginLeft: 10, +}); + +export const noticeContent = style({ + display: "inline-block", // 텍스트가 한 줄로 스크롤되도록 설정 + whiteSpace: "nowrap", // 텍스트를 한 줄로 유지 + fontSize: 16, + fontWeight: 500, + color: "black", + margin: 5, +}); +export const noticeDownBtn = style({ + backgroundImage: 'url("src/assets/downbtn.svg")', // 배경 이미지 경로 + backgroundSize: "cover", // 배경 이미지를 화면 전체에 맞춤 + backgroundPosition: "center", // 이미지를 화면 중앙에 위치 + backgroundRepeat: "no-repeat", // 이미지를 반복하지 않도록 설정 + backgroundColor: "white", + width: 30, + height: 30, + position: "absolute", + right: 10, + border: "none", +}); + +// 공지사항 상세 내용 (애니메이션 효과 포함) +export const noticeDetail = style({ + overflow: "hidden", + maxHeight: "500px", // 최대 높이를 설정 + backgroundColor: "rgba(255,255,255,0.6)", // 배경색 + padding: "10px", + marginTop: -12, + marginBottom: 5, + width: "90%", +}); diff --git a/vite.config.ts b/vite.config.ts index bf4565d..dd7d7fa 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,7 +2,6 @@ import { defineConfig } from "vite"; import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin"; import react from "@vitejs/plugin-react"; -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), vanillaExtractPlugin()], });