diff --git a/next.config.js b/next.config.js index 147c47a2..241de61e 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + swcMinify: true, images: { domains: [ process.env.NEXT_PUBLIC_DOMAIN, diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e4f6e2b7..33443d15 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,5 @@ import Provider from "@/provider/provider.helper"; +import React from "react"; export const metadata = { title: "BSM", diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 00000000..79fa4050 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { PageNotFound } from "@/assets/images"; +import { Button } from "@/components/atoms"; +import { ROUTER } from "@/constants"; +import { color, flex } from "@/styles"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import React from "react"; +import styled from "styled-components"; + +const NotFound = () => { + const router = useRouter(); + return ( + + + + + ); +}; + +const Container = styled.div` + width: 100%; + height: 70vh; + ${flex.COLUMN_CENTER}; +`; + +const StyledImage = styled(Image)` + width: 50%; + height: fit-content; +`; + +export default NotFound; diff --git a/src/assets/icons/Arrow.tsx b/src/assets/icons/Arrow.tsx index 34cd4f15..b1bc1717 100644 --- a/src/assets/icons/Arrow.tsx +++ b/src/assets/icons/Arrow.tsx @@ -14,9 +14,11 @@ const Arrow = ({ height = 25, color = "#727272", direction = "bottom", + ...props }: SVGAttribute) => { return ( { +const Logo = ({ ...props }: React.SVGProps) => { return ( { }; const StyledSVG = styled.svg` - width: 40px; - height: 40px; - @media screen and (max-width: 1025px) { width: 34px; height: 34px; diff --git a/src/assets/images/index.ts b/src/assets/images/index.ts index 712e7b0e..97f03928 100644 --- a/src/assets/images/index.ts +++ b/src/assets/images/index.ts @@ -8,3 +8,4 @@ export { default as ThinkingFace } from "./thinking_face.png"; export { default as HuggingFace } from "./hugging_face.png"; export { default as TestBanner } from "./test_banner.png"; export { default as TestSmallBanner } from "./test_small_banner.png"; +export { default as PageNotFound } from "./page_not_found.png"; diff --git a/src/assets/images/page_not_found.png b/src/assets/images/page_not_found.png new file mode 100644 index 00000000..453f1cd0 Binary files /dev/null and b/src/assets/images/page_not_found.png differ diff --git a/src/components/common/Aside/InfomationBox.tsx b/src/components/common/Aside/InfomationBox.tsx index 9663a54d..ad250ef8 100644 --- a/src/components/common/Aside/InfomationBox.tsx +++ b/src/components/common/Aside/InfomationBox.tsx @@ -1,13 +1,15 @@ import styled from "styled-components"; -import Link from "next/link"; import { color, font } from "@/styles"; -import { USER, ROUTER } from "@/constants"; +import { USER } from "@/constants"; import { IUser } from "@/interfaces"; import { Row, Column } from "@/components/Flex"; import { getUserRole } from "@/helpers"; import flex from "@/styles/flex"; import { ImageWithFallback } from "@/components/atoms"; import { defaultProfile } from "@/assets/images"; +import useModal from "@/hooks/useModal"; +import { useRouter } from "next/navigation"; +import LoginModal from "../Modal/LoginModal"; interface IInfomationBoxProps { user: IUser; @@ -15,7 +17,16 @@ interface IInfomationBoxProps { } const InfomationBox = ({ user, isLogined }: IInfomationBoxProps) => { - const ifLoginedStudent = isLogined && user.role === USER.STUDENT; + const router = useRouter(); + const isLoginedStudent = isLogined && user.role === USER.STUDENT; + const { openModal } = useModal(); + + const handleLoginButtonClick = () => { + if (isLogined) router.push("/"); + openModal({ + component: , + }); + }; return ( @@ -29,7 +40,7 @@ const InfomationBox = ({ user, isLogined }: IInfomationBoxProps) => { rounded /> )} - {ifLoginedStudent && ( + {isLoginedStudent && ( <> @@ -42,15 +53,15 @@ const InfomationBox = ({ user, isLogined }: IInfomationBoxProps) => { {getUserRole(user.role)} - 내 정보 + + 내 정보 + )} {!isLogined && ( <> 로그인이 필요해요 - + 로그인 @@ -123,10 +134,11 @@ const UserType = styled.span` } `; -const InfomationButton = styled(Link)` +const InfomationButton = styled.button` ${flex.CENTER}; ${font.btn3}; padding: 4px 10px; + border: none; background-color: ${color.primary_blue}; border-radius: 5px; margin-left: auto; diff --git a/src/components/common/Header/Navigation.tsx b/src/components/common/Header/Navigation.tsx index 8897506c..d51d61ba 100644 --- a/src/components/common/Header/Navigation.tsx +++ b/src/components/common/Header/Navigation.tsx @@ -1,29 +1,40 @@ import styled from "styled-components"; import { color, flex, font } from "@/styles"; +import useWindow from "@/hooks/useWindow"; const navigationTypes = [ { name: "학교", + isDisplayNoneAtResponsive: false, }, { name: "기숙사", + isDisplayNoneAtResponsive: false, }, { name: "커뮤니티", + isDisplayNoneAtResponsive: false, }, { name: "기타 목록", + isDisplayNoneAtResponsive: true, }, ]; const Navigation = () => { + const { isWindow } = useWindow(); return ( - {navigationTypes.map((navigation) => ( - - {navigation.name} - - ))} + {isWindow && + navigationTypes.map((navigation) => { + if (navigation.isDisplayNoneAtResponsive && window.innerWidth <= 768) + return; + return ( + + {navigation.name} + + ); + })} ); }; diff --git a/src/components/common/Header/SubNavigation.tsx b/src/components/common/Header/SubNavigation.tsx index d87af9db..a3fe2343 100644 --- a/src/components/common/Header/SubNavigation.tsx +++ b/src/components/common/Header/SubNavigation.tsx @@ -1,6 +1,7 @@ import styled from "styled-components"; import { color, flex, font } from "@/styles"; import Link from "next/link"; +import useWindow from "@/hooks/useWindow"; const navigations = [ { @@ -11,13 +12,15 @@ const navigations = [ { name: "🕐 시간표", href: "/timetable" }, { name: "🗓️ 캘린더", href: "/calender" }, ], + isDisplayNoneAtResponsive: false, }, { key: "기숙사 생활", items: [ - { name: "🚪 입사 체크", href: "https://team-insert.com" }, + { name: "🚪 (미완)", href: "/" }, { name: "☕️ 베르실 예약", href: "/reserve" }, ], + isDisplayNoneAtResponsive: false, }, { key: "커뮤니티", @@ -26,25 +29,34 @@ const navigations = [ { name: "🎋 대나무숲", href: "/bamboo" }, { name: "📊 랭킹(미완)", href: "/rank" }, ], + isDisplayNoneAtResponsive: false, }, { key: "기타", items: [{ name: "💼 외부 서비스", href: "/applications" }], + isDisplayNoneAtResponsive: true, }, ]; const SubNavigation = () => { + const { isWindow } = useWindow(); + return ( - {navigations.map((navigation) => ( - - {navigation.items.map((item) => ( - - {item.name} - - ))} - - ))} + {isWindow && + navigations.map((navigation) => { + if (navigation.isDisplayNoneAtResponsive && window.innerWidth <= 768) + return; + return ( + + {navigation.items.map((item) => ( + + {item.name} + + ))} + + ); + })} ); }; @@ -68,6 +80,14 @@ const SubNavigationListItemLink = styled(Link)` font-weight: 500; width: 90px; cursor: pointer; + + @media screen and (max-width: 570px) { + width: 85px; + } + + @media screen and (max-width: 490px) { + width: 80px; + } `; export default SubNavigation; diff --git a/src/components/common/Header/index.tsx b/src/components/common/Header/index.tsx index 81ec75a3..4f3c73b4 100644 --- a/src/components/common/Header/index.tsx +++ b/src/components/common/Header/index.tsx @@ -16,24 +16,24 @@ const Header = () => { >>(null); const handleMouseEnter = () => { - if (isHover) return setIsHover(true); - setDelayHandeler(setTimeout(() => setIsHover(true), 400)); + if (isHover) return setIsHover((prev) => !prev); + setDelayHandeler(setTimeout(() => setIsHover((prev) => !prev), 300)); }; const handleMouseLeave = () => { if (!isHover && delayHandler) return clearTimeout(delayHandler); - setIsHover(false); + setIsHover((prev) => !prev); }; return ( - + openModal({ component: })} /> @@ -46,6 +46,7 @@ const Header = () => { const HeaderLayout = styled.div` ${flex.COLUMN}; + background-color: green; `; const Layout = styled.div` diff --git a/src/components/common/Modal/EmojiModal/index.tsx b/src/components/common/Modal/EmojiModal/index.tsx index b719adaa..524e17ac 100644 --- a/src/components/common/Modal/EmojiModal/index.tsx +++ b/src/components/common/Modal/EmojiModal/index.tsx @@ -1,5 +1,5 @@ import styled, { css } from "styled-components"; -import { color } from "@/styles"; +import { color, flex } from "@/styles"; import ModalList from "./ModalList"; import ModalHeader from "./ModalHeader"; @@ -17,8 +17,13 @@ const EmojiModal = (direction: IEmojiModalProps) => { return ( <> - - + 아직 지원하지 않는 기능이에요. + {false && ( + <> + + + + )} @@ -35,9 +40,9 @@ const Container = styled.div<{ z-index: 10; width: 30vw; border-radius: 12px; + min-height: 20vh; height: fit-content; - display: flex; - flex-direction: column; + ${flex.COLUMN_CENTER}; background-color: ${color.white}; box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.1); ${({ top, right, bottom, left }) => css` diff --git a/src/components/common/Modal/LoginModal/index.tsx b/src/components/common/Modal/LoginModal/index.tsx new file mode 100644 index 00000000..6325f519 --- /dev/null +++ b/src/components/common/Modal/LoginModal/index.tsx @@ -0,0 +1,52 @@ +import Storage from "@/apis/storage"; +import { Logo } from "@/assets/icons"; +import { ROUTER, TOKEN } from "@/constants"; +import useWindow from "@/hooks/useWindow"; +import { color, flex, font } from "@/styles"; +import { useRouter } from "next/navigation"; +import React from "react"; +import styled from "styled-components"; + +const LoginModal = () => { + const router = useRouter(); + const { isWindow } = useWindow(); + + const handleLoginButtonClick = () => { + if (isWindow) { + Storage.setItem(TOKEN.PATH, window.location.pathname); + router.push(process.env.NEXT_PUBLIC_OAUTH_URL || ROUTER.HOME); + } + }; + + return ( + + + + + ); +}; + +const Container = styled.div` + width: fit-content; + height: fit-content; + padding: 40px 30px; + background-color: ${color.white}; + border-radius: 6px; + ${flex.COLUMN_CENTER}; + gap: 5vh; +`; + +const LoginButton = styled.button` + width: fit-content; + border-radius: 4px; + padding: 8px 14px; + background-color: ${color.primary_blue}; + color: ${color.white}; + ${font.btn3}; + + &:after { + content: "BSM 계정으로 로그인"; + } +`; + +export default LoginModal; diff --git a/src/components/common/Modal/PlanAddModal/index.tsx b/src/components/common/Modal/PlanAddModal/index.tsx index 96bceeac..853e88ff 100644 --- a/src/components/common/Modal/PlanAddModal/index.tsx +++ b/src/components/common/Modal/PlanAddModal/index.tsx @@ -7,6 +7,8 @@ import { useAddCalenderPlanMutation } from "@/templates/calender/services/mutati import { color, flex, font } from "@/styles"; import React from "react"; import styled from "styled-components"; +import { toast } from "react-toastify"; +import getPlanType from "@/helpers/getPlanType.helper"; interface IPlanAddModalProps { date: string; @@ -21,12 +23,16 @@ const PlanAddModal = ({ date }: IPlanAddModalProps) => { const textareaRef = React.useRef(null); const handleAddButtonClick = () => { + if (!title.trim()) return toast.error("내용을 입력해주세요!"); + const gradeClassType = planType === "학급 일정" ? "CLASS" : "GRADE"; + const type = planType === "학교 일정" ? "SCHOOL" : gradeClassType; + mutate({ title, priority: 0, date, color: "#000", - type: planType, + type, grade: user.grade, classNumber: user.classNum, }); @@ -56,8 +62,8 @@ const PlanAddModal = ({ date }: IPlanAddModalProps) => { diff --git a/src/components/common/Modal/SettingModal/SettingBody.tsx b/src/components/common/Modal/SettingModal/SettingBody.tsx index 9e8222d5..a3885359 100644 --- a/src/components/common/Modal/SettingModal/SettingBody.tsx +++ b/src/components/common/Modal/SettingModal/SettingBody.tsx @@ -2,6 +2,8 @@ import React from "react"; import styled from "styled-components"; import { color, font } from "@/styles"; import { Switch } from "@/components/atoms"; +import Storage from "@/apis/storage"; +import useModal from "@/hooks/useModal"; const settingOptions = [ { @@ -9,69 +11,37 @@ const settingOptions = [ options: [ { title: "다크 모드 켜기", - isHaveDescription: false, - }, - { - title: "홈에만 커스텀 백그라운드 적용", isHaveDescription: true, - description: - "홈 화면에서만 커스텀으로 설정한 백그라운드를 적용시킬 수 있어요.", - }, - { - title: "디스플레이 배율 설정", - isHaveDescription: false, + description: "다크 모드를 켜요. 디자인 퀄리티가 낮아질 수 있어요.", + type: "bool", }, ], }, { name: "커뮤니티", options: [ - { - title: "익명 모드 켜놓기", - isHaveDescription: true, - description: "익명 모드를 기본값으로 설정할 수 있어요.", - }, { title: "렌더당 렌더링할 게시글 개수", isHaveDescription: true, description: "한 번의 렌더당 몇 개의 게시글을 렌더링할 지 정할 수 있어요.", - }, - { - title: "자세한 시간 표시", - isHaveDescription: true, - description: - "게시글이나 댓글에서 시간, 분, 초 등의 자세한 시간을 표시해요.", - }, - { - title: "모든 답글 표시", - isHaveDescription: true, - description: "모든 답글을 항상 표시해요. 답글이 너무 깊을 수 있어요.", - }, - { - title: "딥웹 모드", - isHaveDescription: true, - description: "예전 BSM의 매운 맛을 즐길 수 있어요.", - }, - ], - }, - { - name: "알림 설정", - options: [ - { - title: "웹 사이트 알림 받기", - isHaveDescription: true, - description: "급식 등의 여러가지 서비스 알림을 구독할 수 있어요.", + type: "input", }, ], }, ]; const SettingBody = () => { - const [isSwitch, setIsSwitch] = React.useState(false); + const { closeModal } = useModal(); + const [darkMode, setDarkMode] = React.useState(!!Storage.getItem("theme")); + const [postLimit, setPostLimit] = React.useState( + Storage.getItem("post_render_limit") || 20, + ); - const handleToggleButton = () => { - setIsSwitch(!isSwitch); + const handleSaveButtonClick = () => { + if (darkMode) Storage.setItem("theme", "dark"); + Storage.setItem("post_render_limit", `${postLimit}`); + closeModal(); }; return ( @@ -89,13 +59,31 @@ const SettingBody = () => { )} - - - + {option.type === "bool" && ( + + setDarkMode((prev) => !prev)} + /> + + )} + {option.type === "input" && ( + + + !Number.isNaN(+e.target.value) && + setPostLimit(e.target.value) + } + /> + + + )} ))} ))} + ); }; @@ -147,4 +135,40 @@ const SwitchBox = styled.div` margin-left: auto; `; +const StyledInputBox = styled.div` + margin-left: auto; + display: flex; + align-items: center; + gap: 6px; +`; + +const StyledInput = styled.input` + max-width: 70px; + text-align: right; + padding: 4px 8px; + background-color: ${color.white}; + ${font.p4}; + box-shadow: 2px 2px 15px 0 rgba(0, 0, 0, 0.1); +`; + +const StyledSummary = styled.span` + ${font.p3}; + &:after { + content: "개"; + } +`; + +const SaveButton = styled.button` + margin-left: auto; + padding: 6px 18px; + background-color: ${color.primary_blue}; + color: ${color.white}; + ${font.p4}; + border-radius: 4px; + + &:after { + content: "저장하기"; + } +`; + export default SettingBody; diff --git a/src/components/common/Modal/SettingModal/index.tsx b/src/components/common/Modal/SettingModal/index.tsx index 7c898568..d0f2abd1 100644 --- a/src/components/common/Modal/SettingModal/index.tsx +++ b/src/components/common/Modal/SettingModal/index.tsx @@ -14,7 +14,7 @@ const SettingModal = () => { const Container = styled.div` width: 40vw; - height: 90vh; + height: fit-content; overflow-y: scroll; background-color: ${color.white}; box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); diff --git a/src/constants/token.constant.ts b/src/constants/token.constant.ts index 958ec543..7eeaecf0 100644 --- a/src/constants/token.constant.ts +++ b/src/constants/token.constant.ts @@ -1,11 +1,16 @@ export interface IToken { - [TOKEN: string]: "access_token" | "refresh_token" | "post_render_limit"; + [TOKEN: string]: + | "access_token" + | "refresh_token" + | "post_render_limit" + | "path"; } const TOKEN: IToken = { ACCESS: "access_token", REFRESH: "refresh_token", POST_RENDER_LIMIT: "post_render_limit", + PATH: "path", } as const; export default TOKEN; diff --git a/src/helpers/checkPostValid.helper.ts b/src/helpers/checkPostValid.helper.ts index 1551c360..9019d50e 100644 --- a/src/helpers/checkPostValid.helper.ts +++ b/src/helpers/checkPostValid.helper.ts @@ -20,27 +20,27 @@ const checkPostValid = (post: IInputPost) => { category === POST.LOST || category === POST.FOUND; // 공통사항 - if (!title) return toast.error("글 제목을 입력해주세요."); + if (!title.trim()) return toast.error("글 제목을 입력해주세요."); // 일반 혹은 공지사항 카테고리일 경우 content 유효성 검사 - if (is일반혹은공지사항카테고리라면 && !content) + if (is일반혹은공지사항카테고리라면 && !content.trim()) return toast.error("글 내용을 입력해주세요."); // 코드리뷰 카테고리일 경우 PR 링크 유효성 검사 - if (category === POST.CODE_REVIEW && !prUrl) + if (category === POST.CODE_REVIEW && !prUrl.trim()) return toast.error("PR 링크를 입력해주세요."); // 프로젝트 카테고리일 경우 if (category === POST.PROJECT) { if (!startTime) return toast.error("프로젝트 시작 기한을 입력해주세요."); if (!endTime) return toast.error("프로젝트 마감 기한을 입력해주세요."); - if (!field) return toast.error("프로젝트 분야를 입력해주세요."); + if (!field.trim()) return toast.error("프로젝트 분야를 입력해주세요."); } // 분실물찾기 카테고리일 경우 - if (is분실물찾기카테고리라면 && !place) + if (is분실물찾기카테고리라면 && !place.trim()) return toast.error("장소를 입력해주세요."); - if (category === POST.FOUND && !keepingPlace) + if (category === POST.FOUND && !keepingPlace.trim()) return toast.error("보관 장소를 입력해주세요."); return true; diff --git a/src/helpers/getPlanType.helper.ts b/src/helpers/getPlanType.helper.ts new file mode 100644 index 00000000..79b9e72e --- /dev/null +++ b/src/helpers/getPlanType.helper.ts @@ -0,0 +1,14 @@ +const getPlanType = (plan: string) => { + switch (plan) { + case "CLASS": + return "학급 일정"; + case "GRADE": + return "학년 일정"; + case "SCHOOL": + return "학교 일정"; + default: + return plan; + } +}; + +export default getPlanType; diff --git a/src/hooks/useMeisterHTML.ts b/src/hooks/useMeisterHTML.ts index de51f995..1495d0bb 100644 --- a/src/hooks/useMeisterHTML.ts +++ b/src/hooks/useMeisterHTML.ts @@ -114,10 +114,28 @@ export const useMeisterHTML = () => { }; const pointParser = (html: string) => { - return html.replaceAll( - 'style="border:1px solid #ccc;margin-bottom:10px;border-radius:3px;padding:10px;box-shadow: 2px 2px 1px 2px #ddd;"', - "", - ); + return html + + .replaceAll("\n", "") + .replaceAll("\t", "") + .replaceAll("상점내용 : ", ``) + .replaceAll("벌점내용 : ", ``) + .replaceAll("`", "") + .replaceAll( + "(상점 : ", + `\n상점 `, + ) + .replaceAll( + "(벌점 : ", + `\n벌점 `, + ) + .replaceAll("점)", "점") + .replaceAll("부여일 : ", "20") + .replace(/상\d{2}-/gi, "") + .replace(/기숙사\d{2}-/gi, "") + .replace(/학교\d{2}-/gi, "") + .replace(/\(([^)]*선생님[^)]*)\)/g, "$1") + .replace(/·/gi, "asdfasfsf"); }; return { getBasicJobSkills, scoreParser, pointParser }; diff --git a/src/hooks/useTimetableBar.ts b/src/hooks/useTimetableBar.ts index 2c1e6683..6030e7b4 100644 --- a/src/hooks/useTimetableBar.ts +++ b/src/hooks/useTimetableBar.ts @@ -1,10 +1,12 @@ import React from "react"; import useDate from "@/hooks/useDate"; +import useWindow from "./useWindow"; const useTimetableBar = () => { const date = useDate(); const [nowDate, setNowDate] = React.useState(""); const [isScrollBox, setIsScrollBox] = React.useState(false); + const { isWindow } = useWindow(); const scrollRef = React.useRef(null); const intervalRef = React.useRef(null); @@ -32,7 +34,8 @@ const useTimetableBar = () => { const synchronizeCurrentTime = () => { if (intervalRef.current && isScrollBox) return; - intervalRef.current = window.setInterval(현재시간과동기화, 1000); + if (isWindow) + intervalRef.current = window.setInterval(현재시간과동기화, 1000); }; const handleTimetableButtonClick = () => { @@ -41,7 +44,7 @@ const useTimetableBar = () => { }; React.useEffect(() => { - if (isScrollBox) { + if (isScrollBox && isWindow) { window.clearInterval(intervalRef.current as number); intervalRef.current = null; } diff --git a/src/provider/layoutProvider.helper.tsx b/src/provider/layoutProvider.helper.tsx index c612e43e..b46f74ec 100644 --- a/src/provider/layoutProvider.helper.tsx +++ b/src/provider/layoutProvider.helper.tsx @@ -1,6 +1,5 @@ -import { Column } from "@/components/Flex"; import { Footer, Header, Modal } from "@/components/common"; -import { GlobalStyle } from "@/styles"; +import { GlobalStyle, flex } from "@/styles"; import React from "react"; import { ToastContainer, toast } from "react-toastify"; import styled from "styled-components"; @@ -12,11 +11,11 @@ const LayoutProvider = ({ children }: React.PropsWithChildren) => { - +
- {children} + {children}