Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
20afe6d
채팅창 높이 지정 (#307)
haegu97 Jan 10, 2025
50180e4
토스트가 사라지지 않는 버그 수정 #309 (#311)
haegu97 Jan 10, 2025
6c3d353
♻️[Refactor] 토스트 메시지 상수로 관리 #312 (#314)
haegu97 Jan 10, 2025
0fc6d1e
♻️ [Refactor] 프로그레스바 넘침 현상 해결 (#313)
cloud0406 Jan 10, 2025
58426ac
✨[Feat] ssr로 데이터 fetching #308
wynter24 Jan 13, 2025
8522c30
✨[Feat] ssr로 fetching 한 데이터를 useQuery 초기데이터로 설정 #308
wynter24 Jan 14, 2025
cbaa135
✨[Feat] srr로 data fetching 후 csr로 data fetching 작업 중 #308
wynter24 Jan 15, 2025
86b7761
📝[Docs] 리드미 포맷 적용 (#319)
cloud0406 Jan 17, 2025
9b61a24
✨[Feat] ssr로 가져온 데이터로 렌더링 #308
wynter24 Jan 18, 2025
6d785dc
✨[Feat] ssr로 데이터 fetching #308
wynter24 Jan 13, 2025
b90275f
✨[Feat] ssr로 fetching 한 데이터를 useQuery 초기데이터로 설정 #308
wynter24 Jan 14, 2025
f22837a
✨[Feat] srr로 data fetching 후 csr로 data fetching 작업 중 #308
wynter24 Jan 15, 2025
06344da
✨[Feat] ssr로 가져온 데이터로 렌더링 #308
wynter24 Jan 18, 2025
e27baf4
Merge branch '308-refactor-서버-측에서-초기-10개의-데이터를-가져와-렌더링' of https://gi…
wynter24 Jan 18, 2025
07a6a75
💄[Design] 모임 카드 size=100으로 요청 #308
wynter24 Jan 20, 2025
1887f7e
✅[Test] fetchBookClub 테스트 코드 작성 #308
wynter24 Jan 20, 2025
3e3ff44
로그인 테스트 코드 추가 #317 (#318)
haegu97 Jan 21, 2025
da9f989
✅[Test] useBookClubList 훅 테스트 코드 작성 (쿼리키로 데이터 가져오기 검증) #308
wynter24 Jan 22, 2025
bc44663
✨[Feat] ssr로 데이터 fetching #308
wynter24 Jan 13, 2025
ecd7a98
✨[Feat] ssr로 fetching 한 데이터를 useQuery 초기데이터로 설정 #308
wynter24 Jan 14, 2025
c37ebec
✨[Feat] srr로 data fetching 후 csr로 data fetching 작업 중 #308
wynter24 Jan 15, 2025
8b5f007
✨[Feat] ssr로 가져온 데이터로 렌더링 #308
wynter24 Jan 18, 2025
6c2a3ae
✨[Feat] srr로 data fetching 후 csr로 data fetching 작업 중 #308
wynter24 Jan 15, 2025
27a8cb6
✨[Feat] ssr로 가져온 데이터로 렌더링 #308
wynter24 Jan 18, 2025
01575e7
💄[Design] 모임 카드 size=100으로 요청 #308
wynter24 Jan 20, 2025
e5455fa
✅[Test] fetchBookClub 테스트 코드 작성 #308
wynter24 Jan 20, 2025
0361c70
✅[Test] useBookClubList 훅 테스트 코드 작성 (쿼리키로 데이터 가져오기 검증) #308
wynter24 Jan 22, 2025
54a800f
Merge branch '308-refactor-서버-측에서-초기-10개의-데이터를-가져와-렌더링' of https://gi…
wynter24 Jan 22, 2025
15fc754
🐛[Fix] SSR 환경에서 서버 환경변수(API_URL) 누락 문제 해결 #308
wynter24 Jan 24, 2025
f7b9b74
💬[Comment] 주석 추가 #308
wynter24 Jan 24, 2025
24cf4e7
✅[Test] 환경 변수 에러 확인 #308
wynter24 Jan 24, 2025
6018847
✅[Test] 환경 변수 에러 확인 #308
wynter24 Jan 24, 2025
c037327
♻️[Refactor] 환경 변수 주소로 수정 #308
wynter24 Jan 24, 2025
72f52f7
📦[Chore] next.config env 설정 #308
wynter24 Jan 24, 2025
3eb1c52
📦[Chore] CI 환경설정: NEXT_PUBLIC_API_URL 전달 설정 추가 #308
wynter24 Jan 24, 2025
d321aec
💬[Comment] 환경 변수 test 주석 제거 #308
wynter24 Jan 25, 2025
00b888e
♻️[Refactor] fetchBookClub 테스트 코드 공통 mockBookClubs 사용 #308
wynter24 Jan 25, 2025
169e0c5
♻️[Refactor] fetchBookClubs 에러 개발 환경에서만 로그 출력 #308
wynter24 Jan 25, 2025
7780462
🔥[Remove] 불필요한 feature/bookclub/api 삭제 #308
wynter24 Jan 25, 2025
08c72b9
♻️[Refactor] initialData 방식 대신 prefetchQuery로 서버에서 데이터 가져오기 #308
wynter24 Feb 4, 2025
1054f49
♻️[Refactor] 찜하기 후 mutate되는 모임 목록의 쿼리키 변경 #308
wynter24 Feb 4, 2025
6af325f
✅[Test] fetchBookClubs 함수 수정에 따른 테스트 코드 업데이트 #308
wynter24 Feb 4, 2025
86fcf78
🐛[Test] Storybook에서 useRouter Mocking하여 오류 해결 #308
wynter24 Feb 4, 2025
17a2e67
🐛[Test] Storybook에서 useRouter Mocking 코드 제거거 #308
wynter24 Feb 4, 2025
280d347
🐛[Fix] storybook router 에러 fix (#334)
cloud0406 Feb 6, 2025
00dbfab
Merge branch 'develop' of https://github.com/codeit-team3/FE into 308…
wynter24 Feb 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ jobs:
with:
node-version: ${{ matrix.node-version }}

- name: Set environment variables
run: echo "NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}" >> .env

- name: Install dependencies
run: npm install

Expand All @@ -35,6 +38,8 @@ jobs:
run: npm test

- name: Build Next.js app
env:
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
run: npm run build

- name: Build Storybook
Expand Down
123 changes: 100 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,113 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
<div align="center">
<img width="652" alt="image" src="https://github.com/user-attachments/assets/ddde3e13-b5d5-4dc0-880b-29dec240bdd1" />

## Getting Started
> 📖 당신의 독서 생활에 새로운 페이지를 열어보세요!
<br/> 새로운 사람들과 함께 읽고 나누는 특별한 독서 경험, **북코**가 함께합니다.
> <br/>
<br/>[![Bookco](https://img.shields.io/badge/BOOKCO.SITE-00a991?style=for-the-badge)](https://bookco.vercel.app/)
</div

First, run the development server:
<br/>
<br/>
<br/>

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
## 🎯 Bookco에서 할 수 있는 일

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
- **👥 독서 모임**

비슷한 취향을 가진 사람들과 함께 책을 읽고 이야기를 나눌 수 있습니다.
- 정해진 책으로 독서 모임에 참여하거나, 직접 모임을 만들 수 있습니다.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
- 💬 **채팅하기**

다른 북코 유저들과 채팅 기능을 통해 소통할 수 있습니다.
- 모임의 호스트나 교환하고 싶은 책을 가진 유저와 대화를 나눌 수 있습니다.

- **📚 교환하기 (추후 개발 예정..)**

안 보게 된 책을 등록하면, 다른 사람의 책과 바꿔 읽을 수 있습니다.
- 집에서 방치되던 책을 다른 유저와 공유할 수 있습니다.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
<br/>
<br/>

## Learn More
## 📚 서비스 소개

To learn more about Next.js, take a look at the following resources:
<img width="1328" alt="image" src="https://github.com/user-attachments/assets/25ef2ed8-3cef-4b7a-b897-2f95324de9d9" />
<img width="1325" alt="image" src="https://github.com/user-attachments/assets/3f812c80-5b20-45ac-8c21-86bb87e76856" />
<img width="1329" alt="image" src="https://github.com/user-attachments/assets/25d9a7f6-8419-4f0b-86d3-f3d11ac38afa" />
<img width="1327" alt="image" src="https://github.com/user-attachments/assets/3a293a66-b502-4be0-a6d8-b02ed88f233f" />
<img width="1325" alt="image" src="https://github.com/user-attachments/assets/4b90f89f-b3d6-4331-8429-4a2280879f58" />
<img width="1324" alt="image" src="https://github.com/user-attachments/assets/4257b49f-c552-4963-866f-7c3338ab307b" />
<img width="1323" alt="image" src="https://github.com/user-attachments/assets/545fc19d-7ed6-4848-afb0-7452563bf8fb" />
<img width="1325" alt="image" src="https://github.com/user-attachments/assets/c751accd-74ec-4258-b285-5a976562759e" />
<img width="1326" alt="image" src="https://github.com/user-attachments/assets/f7824ee7-864b-4671-a889-d7181701ee9e" />

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
<br/>
<br/>

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## 🛠️ 기술스택

## Deploy on Vercel
### 💻 Core
![Next.js](https://img.shields.io/badge/Next.js-000000?style=for-the-badge&logo=next.js&logoColor=white)
![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)

### 🔄 상태 관리
![TanStack Query](https://img.shields.io/badge/TanStack_Query-FF4154?style=for-the-badge&logo=reactquery&logoColor=white)
![Zustand](https://img.shields.io/badge/Zustand-000000?style=for-the-badge)

### 🌐 통신
![Axios](https://img.shields.io/badge/Axios-5A29E4?style=for-the-badge&logo=axios&logoColor=white)
![SockJS](https://img.shields.io/badge/SockJS-000000?style=for-the-badge&logo=socket.io&logoColor=white)
![STOMP](https://img.shields.io/badge/STOMP-000000?style=for-the-badge)

### 🎨 스타일링
![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-06B6D4?style=for-the-badge&logo=tailwindcss&logoColor=white)

### ⚙️ 유틸리티
![Zod](https://img.shields.io/badge/Zod-3E67B1?style=for-the-badge&logo=zod&logoColor=white)

### 🧪 테스팅
![Jest](https://img.shields.io/badge/Jest-C21325?style=for-the-badge&logo=jest&logoColor=white)
![React Testing Library](https://img.shields.io/badge/React_Testing_Library-E33332?style=for-the-badge&logo=testing-library&logoColor=white)
![Storybook](https://img.shields.io/badge/Storybook-FF4785?style=for-the-badge&logo=storybook&logoColor=white)

### 📋 코드 품질
![ESLint](https://img.shields.io/badge/ESLint-4B32C3?style=for-the-badge&logo=eslint&logoColor=white)
![Prettier](https://img.shields.io/badge/Prettier-F7B93E?style=for-the-badge&logo=prettier&logoColor=black)
![Husky](https://img.shields.io/badge/Husky-000000?style=for-the-badge)

<br/>
<br/>

## 🤝 팀 협업 방식, 브랜치 전략

### ✅ **PR 리뷰 방식**
- **2명 Approve** 방식
- PR 확인 시간 고정: `09:00`, `13:00`, `18:00`
- **Pn 룰**과 **Dn 룰** 적용
- **데일리 스크럼** 진행

### ✅ **브랜치 전략**
- **GitHub Flow** 적용
- `feature` → `develop` → `main`
- `hotfix` 는 Main에서 급하게 수정할 일 있을 때 사용

### ✅ **CI/CD 전략**
- **Husky**를 통한 코드 품질 관리
- 커밋시 린트 검사
- **디스코드 웹훅 연결**로 실시간 알림
- PR 작성시 Lint 검사, test 코드 실행, 스토리북 빌드, 프로덕션 빌드 실행하여 검사

<br/>
<br/>

## 👥 팀원 구성

|FE|FE|FE|FE|
|:---:|:---:|:---:|:---:|
|<img src="https://avatars.githubusercontent.com/u/108677235?v=4" width="150"/>|<img src="https://avatars.githubusercontent.com/u/97824352?v=4" width="150"/>|<img src="https://avatars.githubusercontent.com/u/104830526?v=4" width="150"/>|<img src="https://avatars.githubusercontent.com/u/32586926?v=4" width="150"/>|
|[김선구](https://github.com/haegu97)|[김민경](https://github.com/wynter24)|[신선](https://github.com/sunnwave)|[김정호](https://github.com/cloud0406)|
<br>

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom',
// setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
Expand Down
11 changes: 9 additions & 2 deletions src/api/auth/react-query/customHooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMutation } from '@tanstack/react-query';
import { showToast } from '@/components/toast/toast';
import { TOAST_MESSAGES } from '@/constants/messages/toast';
import { authClientAPI } from '../authClientAPI';
import { getUserInfo } from '@/features/auth/api/auth';

Expand All @@ -9,10 +10,16 @@ export function useEditInfoMutation() {
mutationFn: (formData: FormData) => authClientAPI.editInfo(formData),
onSuccess: () => {
getUserInfo();
showToast({ message: '프로필 수정이 완료되었습니다.', type: 'success' });
showToast({
message: TOAST_MESSAGES.SUCCESS.PROFILE_EDIT,
type: 'success',
});
},
onError: (error) => {
showToast({ message: '프로필 수정을 실패하였습니다', type: 'error' });
showToast({
message: TOAST_MESSAGES.ERROR.PROFILE_EDIT_FAILED,
type: 'error',
});
console.error(error);
},
});
Expand Down
46 changes: 29 additions & 17 deletions src/api/book-club/react-query/customHooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { bookClubs } from './queries';
import { showToast } from '@/components/toast/toast';
import { TOAST_MESSAGES } from '@/constants/messages/toast';
import {
bookClubLikeAPI,
bookClubMainAPI,
Expand All @@ -10,6 +11,7 @@ import {
import { WriteReviewParams } from '../types';
import { AxiosError } from 'axios';
import { likeOnError, likeOnMutate } from './likeOptimisticUpdate';
import { BookClubParams } from '@/types/bookclubs';

export function useBookClubCreateMutation() {
const queryClient = useQueryClient();
Expand All @@ -25,7 +27,10 @@ export function useBookClubCreateMutation() {
});
},
onError: () => {
showToast({ message: '북클럽 생성에 실패했습니다.', type: 'error' });
showToast({
message: TOAST_MESSAGES.ERROR.CLUB_CREATE_FAILED,
type: 'error',
});
},
});
}
Expand Down Expand Up @@ -74,12 +79,18 @@ export function useWriteReview() {
queryClient.invalidateQueries({
queryKey: bookClubs.my()._ctx.reviews().queryKey,
});
showToast({ message: '리뷰 작성을 완료하였습니다', type: 'success' });
showToast({
message: TOAST_MESSAGES.SUCCESS.REVIEW_CREATE,
type: 'success',
});
},
onError: (error) => {
console.error(error);

showToast({ message: '리뷰 작성을 실패하였습니다.', type: 'error' });
showToast({
message: TOAST_MESSAGES.ERROR.REVIEW_CREATE_FAILED,
type: 'error',
});
},
});
}
Expand All @@ -103,21 +114,22 @@ export function useCancelBookClub() {
});
}

export function useLikeBookClub() {
export function useLikeBookClub(filter: BookClubParams) {
const queryClient = useQueryClient();

return useMutation<void, AxiosError<{ message: string }>, number>({
mutationFn: (id: number) => bookClubLikeAPI.like(id),

onMutate: async (id) => {
return likeOnMutate(queryClient, id, true);
return likeOnMutate(queryClient, id, true, filter);
},
//TODO: 로직 확인 후 변경 필요
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: bookClubs._def,
});
},
// onSuccess: () => {
// queryClient.invalidateQueries({
// queryKey: ['bookClubs', 'list', DEFAULT_FILTERS],
// });
// // console.log(bookClubs._def)
// },

onError: (_error, id, context) => {
if (context) {
Expand All @@ -127,21 +139,21 @@ export function useLikeBookClub() {
});
}

export function useUnLikeBookClub() {
export function useUnLikeBookClub(filter: BookClubParams) {
const queryClient = useQueryClient();

return useMutation<void, AxiosError<{ message: string }>, number>({
mutationFn: (id: number) => bookClubLikeAPI.unlike(id),

onMutate: async (id) => {
return likeOnMutate(queryClient, id, false);
return likeOnMutate(queryClient, id, false, filter);
},
//TODO: 로직 확인 후 변경 필요
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: bookClubs._def,
});
},
// onSuccess: () => {
// queryClient.invalidateQueries({
// queryKey: bookClubs._def,
// });
// },

onError: (_error, id, context) => {
if (context) {
Expand Down
40 changes: 20 additions & 20 deletions src/api/book-club/react-query/likeOptimisticUpdate.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
import { QueryClient } from '@tanstack/react-query';
import { BookClub } from '@/types/bookclubs';
import { BookClub, BookClubParams } from '@/types/bookclubs';
import { bookClubs } from './queries';
import { DEFAULT_FILTERS } from '@/lib/constants/filters';

export const likeOnMutate = async (
queryClient: QueryClient,
id: number,
isLiked: boolean,
filter?: BookClubParams,
) => {
const listQueryKey = bookClubs.list(DEFAULT_FILTERS).queryKey;
const listQueryKey = ['bookClubs', 'list', filter || DEFAULT_FILTERS];
const detailQueryKey = bookClubs.detail(id).queryKey;

const previousBookClubs = queryClient.getQueryData<{
bookClubs: BookClub[];
}>(listQueryKey);
await queryClient.cancelQueries({ queryKey: listQueryKey });
await queryClient.cancelQueries({ queryKey: detailQueryKey });

// console.log('🔍 수정된 listQueryKey:', listQueryKey);
// console.log('🔍 현재 활성화된 모든 쿼리키:', queryClient.getQueriesData({}));

const previousBookClubs = queryClient.getQueryData<{ bookClubs: BookClub[] }>(
listQueryKey,
);
const previousDetail = queryClient.getQueryData<BookClub>(detailQueryKey);

// 목록 캐시 업데이트
// if (!previousBookClubs) {
// console.warn('⚠️ 캐시된 데이터가 없습니다. queryKey 확인 필요:', listQueryKey);
// queryClient.invalidateQueries({ queryKey: listQueryKey });
// }

if (previousBookClubs) {
queryClient.setQueryData(listQueryKey, {
...previousBookClubs,
bookClubs: previousBookClubs.bookClubs.map((club) =>
queryClient.setQueryData(listQueryKey, (old: any) =>
old?.map((club: BookClub) =>
club.id === id ? { ...club, isLiked } : club,
),
});
);
}

// 상세 캐시 업데이트
if (previousDetail) {
queryClient.setQueryData(detailQueryKey, {
...previousDetail,
isLiked,
});
queryClient.setQueryData(detailQueryKey, { ...previousDetail, isLiked });
}

//TODO: 로직 확인 후 변경 필요
queryClient.invalidateQueries({
queryKey: bookClubs._def,
});

return { previousBookClubs, previousDetail };
};

Expand Down
22 changes: 20 additions & 2 deletions src/app/bookclub/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage';
import { DEFAULT_FILTERS } from '@/lib/constants/filters';
import { fetchBookClubs } from '@/lib/utils/fetchBookClubs';
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query';

export default function Home() {
return <BookClubMainPage />;
export default async function Home() {
const queryClient = new QueryClient();

await queryClient.prefetchQuery({
queryKey: ['bookClubs', 'list', DEFAULT_FILTERS],
queryFn: () => fetchBookClubs(DEFAULT_FILTERS),
});

return (
<HydrationBoundary state={dehydrate(queryClient)}>
<BookClubMainPage />
</HydrationBoundary>
);
}
Loading