diff --git a/backend/build.gradle b/backend/build.gradle index 0a47e8446..93a68ad98 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -19,6 +19,7 @@ sourceCompatibility = '11' ext { snippetsDir = file('build/generated-snippets') + snippetsDir.mkdirs() outputDocs = file('src/main/resources/static/docs') } diff --git a/backend/build_be.sh b/backend/build_be.sh index 548997143..f496a0de5 100755 --- a/backend/build_be.sh +++ b/backend/build_be.sh @@ -36,7 +36,7 @@ fi echo -en $CYAN if [ "$arg" == "build" ] || [ "$arg" == "re" ] || [ "$arg" == "all" ]; then echo "Build with Gradle" - ./gradlew clean build + ./gradlew build -x test fi echo -en $CYAN diff --git a/backend/src/main/java/org/ftclub/cabinet/config/DomainProperties.java b/backend/src/main/java/org/ftclub/cabinet/config/DomainProperties.java index 6224ff457..70f8cb5b9 100644 --- a/backend/src/main/java/org/ftclub/cabinet/config/DomainProperties.java +++ b/backend/src/main/java/org/ftclub/cabinet/config/DomainProperties.java @@ -20,6 +20,9 @@ public class DomainProperties { @Value("${spring.oauth2.domain-name.main}") private String main; + @Value("${spring.server.be-host}") + private String beHost; + @Value("${spring.server.fe-host}") private String feHost; diff --git a/backend/src/main/java/org/ftclub/cabinet/firebase/fcm/service/FCMService.java b/backend/src/main/java/org/ftclub/cabinet/firebase/fcm/service/FCMService.java index 65cbacddd..d8cd9bfb2 100644 --- a/backend/src/main/java/org/ftclub/cabinet/firebase/fcm/service/FCMService.java +++ b/backend/src/main/java/org/ftclub/cabinet/firebase/fcm/service/FCMService.java @@ -5,6 +5,7 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.ftclub.cabinet.config.DomainProperties; import org.ftclub.cabinet.redis.service.RedisService; import org.ftclub.cabinet.utils.overdue.manager.OverdueType; import org.springframework.stereotype.Service; @@ -14,6 +15,8 @@ @Slf4j public class FCMService { private final RedisService redisService; + private final DomainProperties domainProperties; + private static final String ICON_FILE_PATH = "/src/assets/images/logo.svg"; public void sendPushMessage(String name, OverdueType overdueType, Long daysLeftFromExpireDate) { @@ -46,7 +49,9 @@ private void sendOverdueMessage(String token, String name, Long daysLeftFromExpi token, name, daysLeftFromExpireDate); Message message = Message.builder() .putData("title", " 연체 알림") - .putData("content", name + "님, 대여한 사물함이 " + Math.abs(daysLeftFromExpireDate) + "일 연체되었습니다.") + .putData("body", name + "님, 대여한 사물함이 " + Math.abs(daysLeftFromExpireDate) + "일 연체되었습니다.") + .putData("icon", domainProperties.getFeHost() + ICON_FILE_PATH) + .putData("click_action", domainProperties.getFeHost()) .setToken(token) .build(); @@ -63,7 +68,9 @@ private void sendSoonOverdueMessage(String token, String name, Long daysLeftFrom } Message message = Message.builder() .putData("title", " 연체 예정 알림") - .putData("content", "대여한 사물함이 " + daysLeftFromExpireDate + "일 후 연체됩니다.") + .putData("body", "대여한 사물함이 " + daysLeftFromExpireDate + "일 후 연체됩니다.") + .putData("icon", domainProperties.getFeHost() + ICON_FILE_PATH) + .putData("click_action", domainProperties.getFeHost()) .setToken(token) .build(); diff --git a/frontend/package.json b/frontend/package.json index 46d5ac38b..82838cf0b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "cp ../config/frontend/.env .env && vite", "build": "tsc && vite build", "preview": "vite preview", "test": "vitest --reporter verbose", diff --git a/frontend/public/firebase-messaging-sw.js b/frontend/public/firebase-messaging-sw.js index dfc465119..1a1112e4c 100644 --- a/frontend/public/firebase-messaging-sw.js +++ b/frontend/public/firebase-messaging-sw.js @@ -11,10 +11,10 @@ self.addEventListener("push", function (e) { console.log("push: ", e.data.json()); if (!e.data.json()) return; - const resultData = e.data.json().notification; + const resultData = e.data.json().data; const notificationTitle = resultData.title; const notificationOptions = { - body: resultData.body, + body: resultData.content, icon: resultData.image, tag: resultData.tag, ...resultData, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f62ac1da7..740f2a360 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,8 +7,10 @@ import LoginPage from "@/pages/LoginPage"; 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")); const AdminLayout = lazy(() => import("@/pages/admin/AdminLayout")); @@ -29,7 +31,8 @@ function App(): React.ReactElement { } /> } /> } /> - } /> + } /> + } /> {/* admin용 라우터 */} }> diff --git a/frontend/src/assets/images/link.svg b/frontend/src/assets/images/link.svg new file mode 100644 index 000000000..bd9167557 --- /dev/null +++ b/frontend/src/assets/images/link.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/images/profile-circle.svg b/frontend/src/assets/images/profile-circle.svg new file mode 100644 index 000000000..79107f64f --- /dev/null +++ b/frontend/src/assets/images/profile-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx b/frontend/src/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx index 859a9b3fd..ca4c2486f 100644 --- a/frontend/src/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx +++ b/frontend/src/components/LeftNav/LeftMainNav/LeftMainNav.container.tsx @@ -109,6 +109,11 @@ const LeftMainNavContainer = ({ isAdmin }: { isAdmin?: boolean }) => { closeAll(); }; + const onClickProfileButton = () => { + navigator("profile"); + closeAll(); + }; + const onClickLogoutButton = (): void => { const adminToken = isAdmin ? "admin_" : ""; if (import.meta.env.VITE_IS_LOCAL === "true") { @@ -138,6 +143,7 @@ const LeftMainNavContainer = ({ isAdmin }: { isAdmin?: boolean }) => { onClickSearchButton={onClickSearchButton} onClickLogoutButton={onClickLogoutButton} onClickClubButton={onClickClubButton} + onClickProfileButton={onClickProfileButton} isAdmin={isAdmin} /> ); diff --git a/frontend/src/components/LeftNav/LeftMainNav/LeftMainNav.tsx b/frontend/src/components/LeftNav/LeftMainNav/LeftMainNav.tsx index 5a1edc256..0b0211a6a 100644 --- a/frontend/src/components/LeftNav/LeftMainNav/LeftMainNav.tsx +++ b/frontend/src/components/LeftNav/LeftMainNav/LeftMainNav.tsx @@ -10,6 +10,7 @@ interface ILeftMainNav { onClickLentLogButton: React.MouseEventHandler; onClickSearchButton: React.MouseEventHandler; onClickClubButton: React.MouseEventHandler; + onClickProfileButton: React.MouseEventHandler; isAdmin?: boolean; } @@ -23,6 +24,7 @@ const LeftMainNav = ({ onClickLentLogButton, onClickSearchButton, onClickClubButton, + onClickProfileButton, isAdmin, }: ILeftMainNav) => { return ( @@ -58,75 +60,66 @@ const LeftMainNav = ({ {isAdmin && ( - -
- Search -
- )} - {!isAdmin && ( - -
- Log -
- )} - - -
- Contact -
-
- - {isAdmin ? ( - <> - + <> + +
+ Search +
+ +
- Club + Contact
- - ) : ( - +
Club -
- )} -
- - -
- Logout -
+ + +
+ Logout +
+ + )} + {!isAdmin && ( + <> + +
+ Profile +
+ + )}
diff --git a/frontend/src/components/LeftNav/LeftNav.tsx b/frontend/src/components/LeftNav/LeftNav.tsx index 3ff2ab2d4..6aa1b8334 100644 --- a/frontend/src/components/LeftNav/LeftNav.tsx +++ b/frontend/src/components/LeftNav/LeftNav.tsx @@ -2,10 +2,10 @@ import styled from "styled-components"; import LeftMainNavContainer from "@/components/LeftNav/LeftMainNav/LeftMainNav.container"; import LeftSectionNavContainer from "@/components/LeftNav/LeftSectionNav/LeftSectionNav.container"; -const LeftNav: React.FC<{ isVisible: boolean; isAdmin?: boolean }> = ({ - isAdmin, - isVisible, -}) => { +const LeftNav: React.FC<{ + isVisible: boolean; + isAdmin?: boolean; +}> = ({ isAdmin, isVisible }) => { return ( diff --git a/frontend/src/components/LeftNav/LeftSectionNav/LeftSectionNav.container.tsx b/frontend/src/components/LeftNav/LeftSectionNav/LeftSectionNav.container.tsx index 3a48d4816..93fb42c38 100644 --- a/frontend/src/components/LeftNav/LeftSectionNav/LeftSectionNav.container.tsx +++ b/frontend/src/components/LeftNav/LeftSectionNav/LeftSectionNav.container.tsx @@ -1,3 +1,4 @@ +import { useLocation, useNavigate } from "react-router-dom"; import { useRecoilState, useRecoilValue } from "recoil"; import { currentSectionNameState } from "@/recoil/atoms"; import { currentFloorSectionState } from "@/recoil/selectors"; @@ -9,20 +10,54 @@ const LeftSectionNavContainer = ({ isVisible }: { isVisible: boolean }) => { const [currentFloorSection, setCurrentFloorSection] = useRecoilState( currentSectionNameState ); - + const navigator = useNavigate(); + const { pathname } = useLocation(); const { closeLeftNav } = useMenu(); + const isProfilePage: boolean = location.pathname.includes("profile"); const onClickSection = (section: string) => { closeLeftNav(); setCurrentFloorSection(section); }; + const onClickProfile = () => { + closeLeftNav(); + navigator("profile"); + }; + + const onClickLentLogButton = () => { + closeLeftNav(); + navigator("profile/log"); + }; + + const onClickSlack = () => { + window.open( + "https://42born2code.slack.com/archives/C02V6GE8LD7", + "_blank", + "noopener noreferrer" + ); + }; + + const onClickClubForm = () => { + window.open( + "https://docs.google.com/forms/d/e/1FAIpQLSfp-d7qq8gTvmQe5i6Gtv_mluNSICwuv5pMqeTBqt9NJXXP7w/closedform", + "_blank", + "noopener noreferrer" + ); + }; + return ( ); }; diff --git a/frontend/src/components/LeftNav/LeftSectionNav/LeftSectionNav.tsx b/frontend/src/components/LeftNav/LeftSectionNav/LeftSectionNav.tsx index 65a8d23d9..70ebce5f6 100644 --- a/frontend/src/components/LeftNav/LeftSectionNav/LeftSectionNav.tsx +++ b/frontend/src/components/LeftNav/LeftSectionNav/LeftSectionNav.tsx @@ -6,6 +6,12 @@ interface ILeftSectionNav { onClickSection: Function; currentFloorSection: string; floorSection: string[]; + isProfile: boolean; + onClickProfile: Function; + pathname: string; + onClickLentLogButton: Function; + onClickSlack: Function; + onClickClubForm: Function; } const LeftSectionNav = ({ @@ -13,28 +19,76 @@ const LeftSectionNav = ({ currentFloorSection, onClickSection, floorSection, + isProfile, + onClickProfile, + pathname, + onClickLentLogButton, + onClickSlack, + onClickClubForm, }: ILeftSectionNav) => { return ( - - {floorSection.map((section: string, index: number) => ( + <> + + {floorSection.map((section: string, index: number) => ( + onClickSection(section)} + > + {section} + + ))} + + + + onClickSection(section)} + onClick={() => onClickProfile()} > - {section} + 내 정보 - ))} - - + onClickLentLogButton()} + > + 대여 기록 + +
+ onClickSlack()} + title="슬랙 캐비닛 채널 새창으로 열기" + > + 문의하기 + + + onClickClubForm()} + title="동아리 사물함 사용 신청서 새창으로 열기" + > + 동아리 신청서 + + + + ); }; -const LeftNavOptionStyled = styled.div<{ isVisible: boolean }>` +const LeftNavOptionStyled = styled.div<{ + isVisible: boolean; +}>` display: ${(props) => (props.isVisible ? "block" : "none")}; min-width: 240px; height: 100%; @@ -44,6 +98,26 @@ const LeftNavOptionStyled = styled.div<{ isVisible: boolean }>` position: relative; `; +const ProfileLeftNavOptionStyled = styled.div<{ + isProfile: boolean; +}>` + display: ${(props) => (props.isProfile ? "block" : "none")}; + min-width: 240px; + height: 100%; + padding: 32px 10px; + border-right: 1px solid var(--line-color); + font-weight: 300; + position: relative; + & hr { + width: 80%; + height: 1px; + background-color: #d9d9d9; + border: 0; + margin-top: 20px; + margin-bottom: 20px; + } +`; + const FloorSectionStyled = styled.div` width: 100%; height: 40px; @@ -61,4 +135,31 @@ const FloorSectionStyled = styled.div` } `; +const SectionLinkStyled = styled.div` + width: 100%; + height: 40px; + line-height: 40px; + text-indent: 20px; + margin: 2px 0; + padding-right: 30px; + cursor: pointer; + display: flex; + align-items: center; + color: var(--gray-color); + & img { + width: 15px; + height: 15px; + margin-left: auto; + } + @media (hover: hover) and (pointer: fine) { + &:hover { + color: var(--main-color); + } + &:hover img { + filter: invert(33%) sepia(55%) saturate(3554%) hue-rotate(230deg) + brightness(99%) contrast(107%); + } + } +`; + export default LeftSectionNav; diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx new file mode 100644 index 000000000..a111bf2d4 --- /dev/null +++ b/frontend/src/pages/ProfilePage.tsx @@ -0,0 +1,18 @@ +import styled from "styled-components"; + +const ProfilePage = () => { + return 내 정보 여기에 넣어주세용; +}; + +const WrapperStyled = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 70px 0; + @media screen and (max-width: 768px) { + padding: 40px 20px; + } +`; + +export default ProfilePage;