diff --git a/config b/config index 499029fd1..c1c4afb2c 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 499029fd15b4237ce1575038f139f20228c6b186 +Subproject commit c1c4afb2c7a7101f4c8f6e48933a7ad4ea3a4158 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 740f2a360..ece075b5f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,8 +8,6 @@ import MainPage from "@/pages/MainPage"; import AdminMainPage from "@/pages/admin/AdminMainPage"; import LoadingAnimation from "@/components/Common/LoadingAnimation"; import ProfilePage from "./pages/ProfilePage"; -import "./firebase-messaging-sw" - const NotFoundPage = lazy(() => import("@/pages/NotFoundPage")); const LoginFailurePage = lazy(() => import("@/pages/LoginFailurePage")); diff --git a/frontend/src/components/CabinetInfoArea/CabinetInfoArea.container.tsx b/frontend/src/components/CabinetInfoArea/CabinetInfoArea.container.tsx index 02df2ba0d..143de05ad 100644 --- a/frontend/src/components/CabinetInfoArea/CabinetInfoArea.container.tsx +++ b/frontend/src/components/CabinetInfoArea/CabinetInfoArea.container.tsx @@ -70,7 +70,7 @@ export type TModalState = export type TAdminModalState = "returnModal" | "statusModal" | "clubLentModal"; -const calExpiredTime = (expireTime: Date) => +export const calExpiredTime = (expireTime: Date) => Math.floor( (expireTime.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24) ); @@ -102,7 +102,9 @@ const getCabinetUserList = (selectedCabinetInfo: CabinetInfo): string => { return userNameList; }; -const getDetailMessage = (selectedCabinetInfo: CabinetInfo): string | null => { +export const getDetailMessage = ( + selectedCabinetInfo: CabinetInfo +): string | null => { const { status, lentType, lents } = selectedCabinetInfo; // 밴, 고장 사물함 if (status === CabinetStatus.BANNED || status === CabinetStatus.BROKEN) @@ -120,7 +122,9 @@ const getDetailMessage = (selectedCabinetInfo: CabinetInfo): string | null => { else return null; }; -const getDetailMessageColor = (selectedCabinetInfo: CabinetInfo): string => { +export const getDetailMessageColor = ( + selectedCabinetInfo: CabinetInfo +): string => { const { status, lentType, lents } = selectedCabinetInfo; // 밴, 고장 사물함 if (status === CabinetStatus.BANNED || status === CabinetStatus.BROKEN) diff --git a/frontend/src/components/LeftNav/LeftMainNav/LeftMainNav.tsx b/frontend/src/components/LeftNav/LeftMainNav/LeftMainNav.tsx index 7dd21770e..ca630e1d4 100644 --- a/frontend/src/components/LeftNav/LeftMainNav/LeftMainNav.tsx +++ b/frontend/src/components/LeftNav/LeftMainNav/LeftMainNav.tsx @@ -72,53 +72,58 @@ const LeftMainNav = ({ ? "active cabiButton" : " cabiButton" } + src={"/src/assets/images/search.svg"} onClick={onClickSearchButton} > - +
Search - + - +
Contact
- +
Club
- +
Logout
)} {!isAdmin && ( - - - Profile - + <> + +
+ Profile +
+ )} diff --git a/frontend/src/components/LeftNav/LeftSectionNav/LeftSectionNav.tsx b/frontend/src/components/LeftNav/LeftSectionNav/LeftSectionNav.tsx index 8b93607e5..c1f4b532d 100644 --- a/frontend/src/components/LeftNav/LeftSectionNav/LeftSectionNav.tsx +++ b/frontend/src/components/LeftNav/LeftSectionNav/LeftSectionNav.tsx @@ -73,14 +73,14 @@ const LeftSectionNav = ({ title="슬랙 캐비닛 채널 새창으로 열기" > 문의하기 - + onClickClubForm()} title="동아리 사물함 사용 신청서 새창으로 열기" > 동아리 신청서 - + @@ -147,7 +147,7 @@ const SectionLinkStyled = styled.div` display: flex; align-items: center; color: var(--gray-color); - & svg { + & img { width: 15px; height: 15px; margin-left: auto; @@ -155,9 +155,10 @@ const SectionLinkStyled = styled.div` @media (hover: hover) and (pointer: fine) { &:hover { color: var(--main-color); - svg { - stroke: var(--main-color); - } + } + &:hover img { + filter: invert(33%) sepia(55%) saturate(3554%) hue-rotate(230deg) + brightness(99%) contrast(107%); } } `; diff --git a/frontend/src/components/Modals/ExtendModal/ExtendModal.tsx b/frontend/src/components/Modals/ExtendModal/ExtendModal.tsx new file mode 100644 index 000000000..ab2254f00 --- /dev/null +++ b/frontend/src/components/Modals/ExtendModal/ExtendModal.tsx @@ -0,0 +1,140 @@ +import React, { useState } from "react"; +import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"; +import { + currentCabinetIdState, + isCurrentSectionRenderState, + myCabinetInfoState, + targetCabinetInfoState, + userState, +} from "@/recoil/atoms"; +import Modal, { IModalContents } from "@/components/Modals/Modal"; +import ModalPortal from "@/components/Modals/ModalPortal"; +import { + FailResponseModal, + SuccessResponseModal, +} from "@/components/Modals/ResponseModal/ResponseModal"; +import { additionalModalType, modalPropsMap } from "@/assets/data/maps"; +import checkIcon from "@/assets/images/checkIcon.svg"; +import { MyCabinetInfoResponseDto } from "@/types/dto/cabinet.dto"; +import { + axiosCabinetById, + axiosExtendLentPeriod, + axiosMyLentInfo, // axiosExtend, // TODO: 연장권 api 생성 후 연결해야 함 +} from "@/api/axios/axios.custom"; +import { + getExtendedDateString, + getLastDayofMonthString, +} from "@/utils/dateUtils"; + +const ExtendModal: React.FC<{ + onClose: () => void; + cabinetId: Number; +}> = (props) => { + const [showResponseModal, setShowResponseModal] = useState(false); + const [hasErrorOnResponse, setHasErrorOnResponse] = useState(false); + const [modalTitle, setModalTitle] = useState(""); + const currentCabinetId = useRecoilValue(currentCabinetIdState); + const [myInfo, setMyInfo] = useRecoilState(userState); + const [myLentInfo, setMyLentInfo] = + useRecoilState(myCabinetInfoState); + const setTargetCabinetInfo = useSetRecoilState(targetCabinetInfoState); + const setIsCurrentSectionRender = useSetRecoilState( + isCurrentSectionRenderState + ); + const formattedExtendedDate = getExtendedDateString( + myLentInfo.lents ? myLentInfo.lents[0].expiredAt : undefined + ); + const extendDetail = `사물함 연장권 사용 시, + 대여 기간이 ${formattedExtendedDate} 23:59으로 + 연장됩니다. + 연장권 사용은 취소할 수 없습니다. + 연장권을 사용하시겠습니까?`; + const extendInfoDetail = `사물함을 대여하시면 연장권 사용이 가능합니다. +연장권은 ${getLastDayofMonthString( + null, + "/" + )} 23:59 이후 만료됩니다.`; + const getModalTitle = (cabinetId: number | null) => { + return cabinetId === null + ? modalPropsMap[additionalModalType.MODAL_OWN_EXTENSION].title + : modalPropsMap[additionalModalType.MODAL_USE_EXTENSION].title; + }; + const getModalDetail = (cabinetId: number | null) => { + return cabinetId === null ? extendInfoDetail : extendDetail; + }; + const getModalProceedBtnText = (cabinetId: number | null) => { + return cabinetId === null + ? modalPropsMap[additionalModalType.MODAL_OWN_EXTENSION].confirmMessage + : modalPropsMap[additionalModalType.MODAL_USE_EXTENSION].confirmMessage; + }; + const tryExtendRequest = async (e: React.MouseEvent) => { + if (currentCabinetId === 0 || myInfo.cabinetId === null) { + setHasErrorOnResponse(true); + setModalTitle("현재 대여중인 사물함이 없습니다."); + setShowResponseModal(true); + return; + } + try { + await axiosExtendLentPeriod(); + setMyInfo({ + ...myInfo, + cabinetId: currentCabinetId, + extensible: false, + }); + setIsCurrentSectionRender(true); + setModalTitle("연장되었습니다"); + try { + const { data } = await axiosCabinetById(currentCabinetId); + setTargetCabinetInfo(data); + } catch (error) { + throw error; + } + try { + const { data: myLentInfo } = await axiosMyLentInfo(); + setMyLentInfo(myLentInfo); + } catch (error) { + throw error; + } + } catch (error: any) { + setHasErrorOnResponse(true); + setModalTitle(error.response.data.message); + } finally { + setShowResponseModal(true); + } + }; + + const extendModalContents: IModalContents = { + type: myInfo.cabinetId === null ? "penaltyBtn" : "hasProceedBtn", + icon: checkIcon, + title: getModalTitle(myInfo.cabinetId), + detail: getModalDetail(myInfo.cabinetId), + proceedBtnText: getModalProceedBtnText(myInfo.cabinetId), + onClickProceed: + myInfo.cabinetId === null + ? async (e: React.MouseEvent) => { + props.onClose(); + } + : tryExtendRequest, + closeModal: props.onClose, + }; + + return ( + + {!showResponseModal && } + {showResponseModal && + (hasErrorOnResponse ? ( + + ) : ( + + ))} + + ); +}; + +export default ExtendModal; diff --git a/frontend/src/components/TopNav/TopNavButtonGroup/TopNavButtonGroup.tsx b/frontend/src/components/TopNav/TopNavButtonGroup/TopNavButtonGroup.tsx index c9243af63..07b888c35 100644 --- a/frontend/src/components/TopNav/TopNavButtonGroup/TopNavButtonGroup.tsx +++ b/frontend/src/components/TopNav/TopNavButtonGroup/TopNavButtonGroup.tsx @@ -8,12 +8,37 @@ import { } from "@/recoil/atoms"; import TopNavButton from "@/components/TopNav/TopNavButtonGroup/TopNavButton/TopNavButton"; import { CabinetInfo } from "@/types/dto/cabinet.dto"; +import { LentDto } from "@/types/dto/lent.dto"; +import { UserDto } from "@/types/dto/user.dto"; +import CabinetStatus from "@/types/enum/cabinet.status.enum"; +import CabinetType from "@/types/enum/cabinet.type.enum"; import { axiosCabinetById, axiosDeleteCurrentBanLog, } from "@/api/axios/axios.custom"; import useMenu from "@/hooks/useMenu"; +export const getDefaultCabinetInfo = (myInfo: UserDto): CabinetInfo => ({ + building: "", + floor: 0, + cabinetId: 0, + visibleNum: 0, + lentType: CabinetType.PRIVATE, + title: null, + maxUser: 0, + status: CabinetStatus.AVAILABLE, + section: "", + lents: [ + { + userId: myInfo.userId, + name: myInfo.name, + lentHistoryId: 0, + startedAt: new Date(), + expiredAt: new Date(), + }, + ] as LentDto[], + statusNote: "", +}); const TopNavButtonGroup = ({ isAdmin }: { isAdmin?: boolean }) => { const { toggleCabinet, toggleMap, openCabinet, closeAll } = useMenu(); const [currentCabinetId, setCurrentCabinetId] = useRecoilState( diff --git a/frontend/src/pages/admin/SearchPage.tsx b/frontend/src/pages/admin/SearchPage.tsx index 7552e3e2c..92f355210 100644 --- a/frontend/src/pages/admin/SearchPage.tsx +++ b/frontend/src/pages/admin/SearchPage.tsx @@ -61,7 +61,7 @@ const SearchPage = () => { searchValue.current, currentPage.current ); - + setSearchListByIntraId(searchResult.data.result ?? []); setTotalSearchList(Math.ceil(searchResult.data.totalLength / 10) ?? 0); setTimeout(() => { diff --git a/frontend/src/utils/dateUtils.ts b/frontend/src/utils/dateUtils.ts index 81b509b43..f39e1f014 100644 --- a/frontend/src/utils/dateUtils.ts +++ b/frontend/src/utils/dateUtils.ts @@ -1,3 +1,15 @@ +export const padTo2Digits = (num: number) => { + return num.toString().padStart(2, "0"); +}; + +export const formatDate = (date: Date, divider: string) => { + return [ + date.getFullYear(), + padTo2Digits(date.getMonth() + 1), + padTo2Digits(date.getDate()), + ].join(divider); +}; + export const getExpireDateString = ( lentType: string, existExpireDate?: Date @@ -10,19 +22,38 @@ export const getExpireDateString = ( if (!existExpireDate) { expireDate.setDate(expireDate.getDate() + parseInt(addDays)); - } - const padTo2Digits = (num: number) => { - return num.toString().padStart(2, "0"); - }; - const formatDate = (date: Date) => { - return [ - date.getFullYear(), - padTo2Digits(date.getMonth() + 1), - padTo2Digits(date.getDate()), - ].join("/"); - }; - - return formatDate(expireDate); + return formatDate(expireDate, "/"); +}; + +// 공유 사물함 반납 시 남은 대여일 수 차감 (원래 남은 대여일 수 * (남은 인원 / 원래 있던 인원)) +export const getShortenedExpireDateString = ( + lentType: string, + currentNumUsers: number, + existExpireDate: Date | undefined +) => { + if (lentType != "SHARE" || existExpireDate === undefined) return; + const dayInMilisec = 1000 * 60 * 60 * 24; + const expireDateInMilisec = new Date(existExpireDate).getTime(); + let secondUntilExpire = expireDateInMilisec - new Date().getTime(); + let daysUntilExpire = Math.ceil(secondUntilExpire / dayInMilisec) - 1; + let dateRemainig = + (daysUntilExpire * (currentNumUsers - 1)) / currentNumUsers; + let newExpireDate = new Date().getTime() + dateRemainig * dayInMilisec; + return formatDate(new Date(newExpireDate), "/"); +}; + +export const getExtendedDateString = (existExpireDate?: Date) => { + let expireDate = existExpireDate ? new Date(existExpireDate) : new Date(); + expireDate.setDate( + expireDate.getDate() + parseInt(import.meta.env.VITE_EXTENDED_LENT_PERIOD) + ); + return formatDate(expireDate, "/"); +}; + +export const getLastDayofMonthString = (date: Date | null, divider: string) => { + if (date === null) date = new Date(); + let lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0); + return formatDate(lastDay, divider); }; export const getTotalPage = (totalLength: number, size: number) => {