Skip to content

Commit

Permalink
알림창, 마이페이지 드롭다운 공용 컴포넌트 구현 + 알림 기능 추가 (#613)
Browse files Browse the repository at this point in the history
* refactor: 러너, 서포터 마이페이지 분리

* assets: 알림 아이콘 에셋 추가

* feat: Dropdown 공용 컴포넌트 구현

* feat: 알림 컴포넌트 구현

* design: 드롭다운 컴포넌트 z-index 추가

* assets: 마이페이지, 로그아웃 아이콘 에셋 추가

* feat: 드롭다운 메뉴와 트리거와의 gap 설정 props 추가

* design: 마지막 li border-radius 추가

* feat: 드롭다운 컴포넌트 esc, backdrop 클릭 닫기 구현

* feat: 러너, 서포터 마이페이지 라우터 추가

* design: 알림창 반응형 추가

* feat: 프로필 드롭다운 컴포넌트 구현

* feat: 알림, 프로필 클릭 시 드롭다운 되도록 변경

* refactor: button 태그를 p태그로 변경

* fix: 메인 페이지 이외에서 로그아웃시 권한오류 뜨는 것 수정

* chore: cypress open 스크립트 추가

* test: 러너, 서포터 마이페이지 렌더링 테스트 코드 작성

* feat: 드롭다운 컴포넌트 스토리북 추가

* fix: 글 생성 완료 페이지에서 러너 마이페이지로 가도록 변경

* fix: ci 테스트 실패 오류 수정 (명령어 체인 분리)

* design: 버튼 기본 스타일 제거

* design: 삭제 및 스크롤 추가

* feat: 알림 타입 및 게시물로 이동 이벤트 추가

* fix: 분리된 마이페이지로 라우팅 변경

* feat: 러너, 서포터 마이페이지 리액트 쿼리 마이그레이션

* style: 불필요한 공백 삭제

* refactor: Notification -> Alarm으로 네이밍 변경

* feat: 알람 불러오기 및 삭제 기능 추가

* feat: 알림 읽음 patch 구현

* feat: 알람이 없는 경우 빈 알람 아이콘 추가

* feat: 회원탈퇴 버튼 추가

* fix: 아이콘 오타 수정

* refactor: notification으로 네이밍 변경

* refactor: 콜백함수 제거 및 네이밍 변경

* refactor: 불필요한 async 제거

* refactor: 알람 인자로 isLogin 추가

* design: 알람 읽음 표시 추가

* refactor: isLogin 인자 제거
  • Loading branch information
gyeongza authored Oct 10, 2023
1 parent 6b01bf9 commit 34e8da0
Show file tree
Hide file tree
Showing 32 changed files with 968 additions and 209 deletions.
20 changes: 16 additions & 4 deletions frontend/cypress/e2e/app.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ describe('러너 E2E 테스트', () => {

cy.get('div[aria-label="에러 메시지"]').should('be.visible');

cy.get('input[value="주소주소"]').clear().type('https://github.com/woowacourse-teams/2023-baton/pull/1');
cy.get('input[value="주소주소"]').clear();
cy.get('input[value=""]').type('https://github.com/woowacourse-teams/2023-baton/pull/1');

cy.get('button[aria-label="리뷰 요청 글 생성"]').click();

Expand All @@ -78,11 +79,17 @@ describe('러너 E2E 테스트', () => {
cy.get('div[aria-label="알림 메시지"]').should('be.visible');
});

it('마이페이지 게시글을 불러온다', () => {
it('러너 마이페이지 대기중인 리뷰 게시글을 불러온다', () => {
cy.get('img[alt="프로필"]').click();

cy.wait(500);

cy.get('li[aria-label="러너 마이페이지"]').should('be.visible');
cy.get('li[aria-label="서포터 마이페이지"]').should('be.visible');
cy.get('li[aria-label="로그아웃"]').should('be.visible');

cy.get('li[aria-label="러너 마이페이지"]').click();

cy.contains('더보기').click();

cy.wait(500);
Expand All @@ -102,12 +109,17 @@ describe('러너 E2E 테스트', () => {
});
});

it('마이페이지 추가 게시글을 불러온다.', () => {
it('서포터 마이페이지 진행중인 리뷰 게시글을 불러온다.', () => {
cy.get('img[alt="프로필"]').click();

cy.wait(500);

cy.contains('button', '서포터').click();
cy.get('li[aria-label="러너 마이페이지"]').should('be.visible');
cy.get('li[aria-label="서포터 마이페이지"]').should('be.visible');
cy.get('li[aria-label="로그아웃"]').should('be.visible');

cy.get('li[aria-label="서포터 마이페이지"]').click();

cy.contains('button', '진행중인 리뷰').click();

cy.wait(500);
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "webpack-dev-server --config webpack/webpack.dev.js",
"build": "webpack --config webpack/webpack.prod.js",
"test": "cypress run",
"cypress": "cypress open",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"chromatic": "npx chromatic --project-token=chpt_2be44b6342a5cae"
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/apis/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { GetMyPagePostResponse } from '@/types/myPage';
import { PostFeedbackRequest } from '@/types/feedback';
import { GetSupporterCandidateResponse } from '@/types/supporterCandidate';
import { GetNotificationResponse } from '@/types/notification';

export const getRunnerPost = (limit: number, reviewStatus?: ReviewStatus, cursor?: number, tagName?: string) => {
const params = new URLSearchParams({
Expand Down Expand Up @@ -72,6 +73,10 @@ export const getRunnerPostDetail = (runnerPostId: number, isLogin: boolean) => {
return request.get<GetDetailedRunnerPostResponse>(`/posts/runner/${runnerPostId}`, isLogin);
};

export const getNotification = () => {
return request.get<GetNotificationResponse>(`/notifications`, true);
};

export const postRunnerPostCreation = (formData: CreateRunnerPostRequest) => {
const body = JSON.stringify(formData);
return request.post<void>(`/posts/runner`, body);
Expand Down Expand Up @@ -110,10 +115,18 @@ export const patchProposedSupporterSelection = (runnerPostId: number, supporterI
return request.patch<void>(`/posts/runner/${runnerPostId}/supporters`, body);
};

export const patchNotificationCheck = (notificationId: number) => {
return request.patch<void>(`/notifications/${notificationId}`, undefined);
};

export const deleteRunnerPost = (runnerPostId: number) => {
return request.delete<void>(`/posts/runner/${runnerPostId}`);
};

export const deleteNotification = (notificationsId: number) => {
return request.delete<void>(`/notifications/${notificationsId}`);
};

export const postMissionBranchCreation = (repoName: string) => {
const body = JSON.stringify({ repoName });
return request.post<void>(`/branch`, body);
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/assets/logout-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/assets/my-page-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions frontend/src/assets/notification_off.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions frontend/src/assets/notification_on.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useReviewCancelation } from '@/hooks/query/useReviewCancelation';
import { useReviewComplete } from '@/hooks/query/useReviewComplete';
import { usePageRouter } from '@/hooks/usePageRouter';
import useViewport from '@/hooks/useViewport';

import { ReviewStatus } from '@/types/runnerPost';
import React, { useContext } from 'react';
import styled from 'styled-components';
Expand Down Expand Up @@ -42,11 +41,13 @@ const MyPagePostButton = ({ runnerPostId, reviewStatus, isRunner, supporterId, a

const handleClickSupportSelectButton = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();

goToSupportSelectPage(runnerPostId);
};

const handleClickSupportFeedbackButton = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();

if (!supporterId) {
showErrorToast({ title: ERROR_TITLE.ERROR, description: ERROR_DESCRIPTION.NO_SUPPORTER });
return;
Expand Down
161 changes: 161 additions & 0 deletions frontend/src/components/NotificationDropdown/NotificationDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import React from 'react';
import styled from 'styled-components';
import { Notification } from '@/types/notification';
import { usePageRouter } from '@/hooks/usePageRouter';
import { useNotificationDelete } from '@/hooks/query/useNotificationDelete';
import { useNotificationCheck } from '@/hooks/query/useNotificationCheck';

interface Props {
notificationList: Notification[];
}

const NotificationDropdown = ({ notificationList }: Props) => {
const { mutate: deleteNotification } = useNotificationDelete();
const { mutate: patchNotificationCheck } = useNotificationCheck();
const { goToRunnerPostPage } = usePageRouter();

const handlePostClick = (notificationId: number, runnerPostId: number) => {
patchNotificationCheck(notificationId);
goToRunnerPostPage(runnerPostId);
};

const handleDeleteNotification = (e: React.MouseEvent, notificationId: number) => {
e.stopPropagation();

deleteNotification(notificationId);
};

return (
<S.DropdownContainer>
{notificationList?.length > 0 ? (
notificationList?.map((notification) => {
return (
<S.DropdownList
key={notification.notificationId}
onClick={() => handlePostClick(notification.notificationId, notification.referencedId)}
>
<S.NotificationTitleContainer>
<S.NotificationTitle isRead={notification.isRead}>{notification.title}</S.NotificationTitle>
<S.CloseButton onClick={(e) => handleDeleteNotification(e, notification.notificationId)}>
삭제
</S.CloseButton>
</S.NotificationTitleContainer>
<S.NotificationContents>{notification.message}</S.NotificationContents>
<S.NotificationTime>{notification.createdAt}</S.NotificationTime>
</S.DropdownList>
);
})
) : (
<S.EmptyMessage>새로운 알림이 없습니다.</S.EmptyMessage>
)}
</S.DropdownContainer>
);
};

export default NotificationDropdown;

const S = {
DropdownContainer: styled.ul`
width: 414px;
max-height: 427px;
overflow-y: scroll;
& > li {
border-bottom: 1px solid var(--gray-400);
}
& > li:last-child {
border-bottom: none;
border-radius: 0 0 10px 10px;
}
@media (max-width: 768px) {
width: 290px;
}
`,

DropdownList: styled.li`
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px 35px;
cursor: pointer;
&:hover {
background-color: var(--gray-100);
}
`,

NotificationTitleContainer: styled.div`
display: flex;
justify-content: space-between;
`,

CloseButton: styled.button`
font-size: 12px;
&:hover {
color: var(--baton-red);
}
@media (max-width: 768px) {
font-size: 10px;
}
`,

NotificationTitle: styled.p<{ isRead: boolean }>`
font-size: 16px;
font-weight: 700;
position: relative;
${({ isRead }) =>
isRead
? () => ''
: ` &::before {
width: 4px;
height: 4px;
content: '';
position: absolute;
left: -10px;
top: 50%;
transform: translateY(-50%);
background-color: var(--baton-red);
border-radius: 50%;
}`};
@media (max-width: 768px) {
font-size: 14px;
}
`,

NotificationContents: styled.p`
font-size: 14px;
color: var(--gray-700);
@media (max-width: 768px) {
font-size: 12px;
}
`,

NotificationTime: styled.p`
font-size: 12px;
color: var(--gray-700);
text-align: end;
@media (max-width: 768px) {
font-size: 10px;
}
`,

EmptyMessage: styled.p`
height: 427px;
display: flex;
justify-content: center;
align-items: center;
`,
};
77 changes: 77 additions & 0 deletions frontend/src/components/ProfileDropdown/ProfileDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import styled from 'styled-components';
import MyPageIcon from '@/assets/my-page-icon.svg';
import LogoutIcon from '@/assets/logout-icon.svg';
import { useLogin } from '@/hooks/useLogin';
import { usePageRouter } from '@/hooks/usePageRouter';

const ProfileDropdown = () => {
const { logout } = useLogin();
const { goToMainPage, goToRunnerMyPage, goToSupporterMyPage } = usePageRouter();

const handleClickRunnerMyPage = () => {
goToRunnerMyPage();
};

const handleClickSupporterMyPage = () => {
goToSupporterMyPage();
};

const handleClickLogoutButton = () => {
logout();

goToMainPage();
window.location.reload();
};

return (
<S.DropdownContainer>
<S.DropdownList aria-label="러너 마이페이지" onClick={handleClickRunnerMyPage}>
<img width="18px" height="18px" src={MyPageIcon} />
<S.DropdownListTitle>러너 마이페이지</S.DropdownListTitle>
</S.DropdownList>
<S.DropdownList aria-label="서포터 마이페이지" onClick={handleClickSupporterMyPage}>
<img width="18px" height="18px" src={MyPageIcon} />
<S.DropdownListTitle>서포터 마이페이지</S.DropdownListTitle>
</S.DropdownList>
<S.DropdownList aria-label="로그아웃" onClick={handleClickLogoutButton}>
<img width="18px" height="18px" src={LogoutIcon} />
<S.DropdownListTitle>로그아웃</S.DropdownListTitle>
</S.DropdownList>
</S.DropdownContainer>
);
};

export default ProfileDropdown;

const S = {
DropdownContainer: styled.ul`
width: max-content;
height: max-content;
& > li {
border-bottom: 1px solid var(--gray-400);
}
& > li:last-child {
border-bottom: none;
border-radius: 0 0 10px 10px;
}
`,

DropdownList: styled.li`
display: flex;
align-items: center;
gap: 12px;
padding: 15px 25px;
cursor: pointer;
&:hover {
background-color: var(--gray-100);
}
`,

DropdownListTitle: styled.p``,
};
Loading

0 comments on commit 34e8da0

Please sign in to comment.