Skip to content

Commit

Permalink
[Fix] 무한 스크롤 수정 (#107)
Browse files Browse the repository at this point in the history
* fix: useCumtomInfiniteQuery 수정

* feat: 실시간 알림 기능 추가

* docs: README 작성
  • Loading branch information
he2e2 committed Dec 9, 2024
1 parent ea75399 commit dfcb03c
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 18 deletions.
138 changes: 137 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,137 @@
# React + TypeScript + Vite Boilerplate Code
<div align=center>
<img width="336" alt="image" src="https://github.com/user-attachments/assets/7c7f55c9-f4e5-4a9e-ad98-ee54e46303a9">
</div>

<h1 align=center style="text-align: center; font-size: 1.5em">Palette: 팔레트</h3>

<div align=center>

<p>자신만의 경험을 색상과 함께 공유하고,<br>네트워킹을 통해 팔레트를 채워주세요!</p>

https://www.palettee.site/

<br>

<p align=center>
<a href="https://lime-mall-d34.notion.site/Road-to-friendly-2d8db233c6da4aaf8c3696a80ec83555?pvs=4">팀 노션</a>
&nbsp; | &nbsp;
<a href="https://www.figma.com/design/9Xf8cDM0jPx0oGNloEkfKa/Palettee?node-id=0-1&node-type=canvas&t=Sk30i83BB7LXRt0w-0">피그마</a>
&nbsp; | &nbsp;
<a href="https://github.com/prgrms-web-devcourse-final-project/WEB1_1_ZeroOne_FE/wiki">위키</a>
</p>
</div>
<br>

<br>

<!-- TOC end -->

<br>

<!-- TOC --><a name="-"></a>

## ✨ 프로젝트 소개

현대 직장인들은 일주일에 평균 25분 이상을 **자기계발**에 투자하고 있으며, 본업 외에도 사이드 프로젝트를 통해 경력을 확장하려는 경향이 증가하고 있습니다. 그러나 자기계발을 하지 않는 사람들은 **'갓생'** 열풍으로 인해 부담을 느끼고 있습니다. 이러한 상황에서 자기계발을 시작하는 사람들은 혼자 독학하기보다는 다른 사람들과 **함께** 성장하기를 원합니다.<br><br>
**팔레트**는 유저가 포트폴리오를 외부 링크로 연결해 유저의 작업물을 공유할 수 있도록 하며, 협업과 네트워킹에도 최적화된 플랫폼입니다. 아카이브 기능을 통해 다른 사용자들의 이야기를 탐색하고, 게더링 기능을 통해 팀원을 모집하거나 프로젝트에 참여할 수 있습니다. 이러한 기능을 통해 팔레트는 자기계발을 시작하는 사람들이 혼자가 아닌 타인과 함께 성장할 수 있는 환경을 제공하고자 합니다. <br><br>
**팔레트**에서 자신의 경험을 공유하고, 팔레트의 색상을 채워주세요!

<br>

<!-- TOC --><a name="--1"></a>
## 🚀 주요 기능

<!-- TOC --><a name="--2"></a>

### 🙋‍♀️ 나만의 포트폴리오 링크를 공유해주세요

> 나만의 포트폴리오 링크가 있다면, 등록해 다른 유저들과 공유하고 의견을 나눌 수 있어요.
<!-- TOC --><a name="--3"></a>

### 🎨 고유한 색상과 함께 경험을 아카이빙 해보세요

> 색상을 통해 고유한 성격이 존재하는 경험들을 아카이빙할 수 있어요. 경험을 나눠 팔레트를 채워주세요.<br>
> 마크다운 에디터를 통해 자유롭게 나만의 경험을 작성할 수 있습니다!
<!-- TOC --><a name="--4"></a>

### 🗣️ 게더링에서 자기계발 팀원들을 모집해보세요

> 혼자 시작하기 어렵다면, 같이 시작할 팀원들을 구해보세요! 다양한 팀원들이 기다리고 있어요 😊
<br>

<!-- TOC --><a name="--8"></a>

## ⚙️ 기술 스택

<table align=center>
<thead>
<tr>
<th>분류</th>
<th>기술 스택</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<p align=center>Frontend</p>
</td>
<td>
<img src="https://img.shields.io/badge/Vite-646CFF?logo=Vite&logoColor=ffffff">
<img src="https://img.shields.io/badge/React-61DAFB?logo=React&logoColor=ffffff">
<img src="https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=ffffff">
<img src="https://img.shields.io/badge/Sass-D36AC2?logo=sass&logoColor=ffffff">
<img src="https://img.shields.io/badge/Zustand-443E38?logo=react&logoColor=ffffff">
<img src="https://img.shields.io/badge/React%20Query-FF4154?logo=reactquery&logoColor=ffffff">
</td>
</tr>
<tr>
<td>
<p align=center>Deployment</p>
</td>
<td>
<img src="https://img.shields.io/badge/S3-569A31?logo=amazons3&logoColor=ffffff">
<img src="https://img.shields.io/badge/CloudFront-232F3E?logo=amazonaws&logoColor=ffffff">
<img src="https://img.shields.io/badge/Route%2053-4A154B?logo=amazonaws&logoColor=ffffff">
<img src="https://img.shields.io/badge/Netlify-00C7B7?logo=netlify&logoColor=ffffff">
</td>
</tr>
<tr>
<td>
<p align=center>Collaboration</p>
</td>
<td>
<img src="https://img.shields.io/badge/Notion-000000?logo=Notion">
<img src="https://img.shields.io/badge/Figma-F24E1E?logo=Figma&logoColor=ffffff">
<img src="https://img.shields.io/badge/Slack-4A154B?logo=Slack&logoColor=ffffff">
<img src="https://img.shields.io/badge/Discord-4358D8?logo=Discord&logoColor=ffffff">
</td>
</tr>
</tbody>
</table>

<br>

<!-- TOC --><a name="--9"></a>

## 🎨 팀 소개

<table align="center">
<tr align="center">
<th><a href="https://github.com/joarthvr">조천산</a></th>
<th><a href="https://github.com/he2e2">조희정</a></th>
<th><a href="https://github.com/csk6314">채승규</a></th>
</tr>
<tr>
<td><img src="https://github.com/joarthvr.png" width="120" height="120"></td>
<td><img src="https://github.com/he2e2.png" width="120" height="120"></td>
<td><img src="https://github.com/csk6314.png" width="120" height="120"></td>
</tr>
<tr align="center">
<td>Frontend</td>
<td>Frontend</td>
<td>Frontend</td>
</tr>
</table>
8 changes: 5 additions & 3 deletions src/app/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { createRoot } from 'react-dom/client';
import App from './App';
import './styles/globals.scss';

// if (process.env.NODE_ENV === 'development') {
// void worker.start({ onUnhandledRequest: 'warn' });
// }
import { worker } from '@/mocks/browser';

if (process.env.NODE_ENV === 'development') {
void worker.start({ onUnhandledRequest: 'warn' });
}

createRoot(document.getElementById('root')!).render(
<StrictMode>
Expand Down
2 changes: 2 additions & 0 deletions src/features/notification/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './ui';
export * from './notification.hook';
export * from './notification.type';
4 changes: 2 additions & 2 deletions src/shared/hook/useCustomInfiniteQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export const useCustomInfiniteQuery = <
queryKey,
queryFn: ({ pageParam = 0 }) => queryFn({ pageParam: pageParam as number }),
getNextPageParam: (lastPage, allPages) => {
if (Array.isArray(lastPage.data[dataKey][dataKey])) {
const isLastPage = lastPage.data[dataKey][dataKey]?.length < pageSize;
if (Array.isArray(lastPage.data[dataKey])) {
const isLastPage = lastPage.data[dataKey]?.length < pageSize;
return isLastPage ? null : allPages.length;
}
return null;
Expand Down
16 changes: 16 additions & 0 deletions src/widgets/Layout/ui/Header/Header.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,19 @@
border: 1px solid #cfcfcf;
border-radius: 4px;
}

.bell.isNewNotice {
position: relative;
cursor: pointer;

&::after {
position: absolute;
top: -0.2rem;
right: -0.3rem;
width: 0.3rem;
height: 0.3rem;
content: '';
background-color: $red;
border-radius: 50%;
}
}
54 changes: 42 additions & 12 deletions src/widgets/Layout/ui/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import { NAV_LINKS } from '../../constants';

//assets
import { SearchBar } from '@/features';
import { useNotificationList } from '@/features/notification';
import { useUserStore } from '@/features/user/model/user.store';
import Logo from '@/shared/assets/paletteLogo.svg?react';
import { useModalStore } from '@/shared/model/modalStore';
//componen
import { Button, customConfirm } from '@/shared/ui';
import { Button, customConfirm, customToast } from '@/shared/ui';
import { MenuModal } from '@/widgets/MenuModal/MenuModal';
import { NoticeContainer } from '@/widgets/NoticeContainer/NoticeContainer';

Expand All @@ -37,6 +38,9 @@ export const Header = () => {
const noticeRef = useRef<HTMLDivElement>(null);
const [isSearch, setIsSearch] = useState(false);
const [isNotice, setIsNotice] = useState(false);
const [isNewNotice, setIsNewNotice] = useState(false);

const { data: notifications, refetch: fetchNotifications } = useNotificationList();

useEffect(() => {
const handleResize = () => {
Expand All @@ -56,7 +60,7 @@ export const Header = () => {
const EventSource = EventSourcePolyfill || NativeEventSource;
const accessToken = localStorage.getItem('accessToken');

new EventSource(`https://api.palettee.site/notification/subscribe`, {
const eventSource = new EventSource(`https://api.palettee.site/notification/subscribe`, {
headers: {
Authorization: `Bearer ${accessToken}`,
ContentType: 'text/event-stream',
Expand All @@ -65,6 +69,27 @@ export const Header = () => {
},
heartbeatTimeout: 86400000,
});

eventSource.addEventListener('sse', event => {
const eventSource = event as MessageEvent;
if (eventSource.data.startsWith('연결되었습니다.')) return;

void customToast({ text: '새로운 알림이 도착했습니다.', icon: 'info' });
setIsNewNotice(true);
});

fetchNotifications()
.then(() => {
const unreadNoti = notifications?.data.notifications.filter(noti => !noti.isRead);
if (unreadNoti && unreadNoti.length > 0) setIsNewNotice(true);
})
.catch(() => {
void customToast({ text: '알림을 불러오는 중 오류가 발생했습니다.', icon: 'error' });
});

return () => {
eventSource.close();
};
}, [userData]);

useEffect(() => {
Expand Down Expand Up @@ -108,6 +133,7 @@ export const Header = () => {
});
}
if (isSearch) setIsSearch(false);
setIsNewNotice(false);
};

return (
Expand All @@ -127,11 +153,13 @@ export const Header = () => {
icon={faSearch}
onClick={toggleSearch}
/>
<FontAwesomeIcon
className={cn(styles.button, styles.bell)}
icon={faBell}
onClick={toggleNoti}
/>
<div className={cn(styles.bell, { [styles.isNewNotice]: isNewNotice })}>
<FontAwesomeIcon
className={cn(styles.button, styles.bell)}
icon={faBell}
onClick={toggleNoti}
/>
</div>
<FontAwesomeIcon
icon={faBars}
onClick={() => {
Expand Down Expand Up @@ -178,11 +206,13 @@ export const Header = () => {
icon={faSearch}
onClick={toggleSearch}
/>
<FontAwesomeIcon
className={cn(styles.button, styles.bell)}
icon={faBell}
onClick={toggleNoti}
/>
<div className={cn(styles.bell, { [styles.isNewNotice]: isNewNotice })}>
<FontAwesomeIcon
className={cn(styles.button, styles.bell)}
icon={faBell}
onClick={toggleNoti}
/>
</div>
<FontAwesomeIcon
className={cn(styles.button, styles.heart)}
icon={faHeart}
Expand Down

1 comment on commit dfcb03c

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚡ Lighthouse report for http://localhost:3000/

Category Score
🔴 Performance 17
🟠 Accessibility 86
🟢 Best Practices 92
🟠 SEO 85

Detailed Metrics

Metric Value
🔴 First Contentful Paint 57.7 s
🔴 Largest Contentful Paint 100.1 s
🔴 Total Blocking Time 1,930 ms
🟢 Cumulative Layout Shift 0.051
🔴 Speed Index 76.8 s

Please sign in to comment.