Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Next / TypeScript / React Query / Zustand / Vanilla Extract를 사용하여 Real World 1차 기능 구현을 완료합니다. #155

Merged
merged 50 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
f59fd0f
feat: review-assign-action.yml 깃헙 액션을 추가합니다.
InSeong-So Sep 2, 2023
4d80c9f
Update review-assign-action.yml
InSeong-So Sep 7, 2023
e577cd8
Update review-assign-action.yml
InSeong-So Sep 7, 2023
4bedb3a
Update review-assign-action.yml
InSeong-So Sep 7, 2023
6ea21d9
Update review-assign-action.yml
InSeong-So Sep 7, 2023
daad074
Update review-assign-action.yml
InSeong-So Sep 7, 2023
dab9319
feat: useInfiniteQuery test
hyeon9782 Sep 14, 2023
7681666
Merge branch 'team4/jeong-ho'
hyeon9782 Sep 14, 2023
9419f91
refactor & feat: 효율적인 fetch 사용을 위한 fetch 래핑 및 axios에서 fetch로 전환
hyeon9782 Sep 14, 2023
d1f6b43
feat: ArticleList useIntersectionObserver 커스텀 훅을 통한 무한 스크롤 기능 구현
hyeon9782 Sep 15, 2023
89b8039
refactor: 무한 스크롤 기능 useInfiniteScroll로 비즈니스 로직 분리
hyeon9782 Sep 15, 2023
50f225e
refactor: signup , login , logout 기능 route handler를 사용하여 리팩토링 및 middl…
hyeon9782 Sep 18, 2023
fc4f103
feat & refactor: route handler를 이용한 user api 연동
hyeon9782 Sep 18, 2023
32bceb4
feat: react query를 사용한 로그인
hyeon9782 Sep 26, 2023
1dc77fb
feat: tag로 article 검색 기능 구현
hyeon9782 Sep 26, 2023
209d8ab
refactor: useUserStore로 로그인 로그아웃 기능 수정
hyeon9782 Sep 26, 2023
04c1062
feat: useArticles 커스텀 훅을 통해 Articles 관련 데이터 패칭 처리
hyeon9782 Sep 28, 2023
77d9718
fix: useArticles 데이터가 적을 경우 생기는 에러 해결
hyeon9782 Sep 29, 2023
6f9e355
feat: follow 기능 구현
hyeon9782 Sep 29, 2023
d47e250
feat: Banner 컴포넌트 login 했을 때 home화면에서 안보이게 수정
hyeon9782 Sep 29, 2023
b7d3495
feat: 게시글 작성 기능 구현
hyeon9782 Oct 1, 2023
12f44e4
feat: getArticlesWithAuthorAPI, getArticlesWithFavoritedAPI 추가
hyeon9782 Oct 4, 2023
be02273
ffeat: TagList 컴포넌트 기능 수정 및 useCurrentTab 타입 추가
hyeon9782 Oct 4, 2023
a761449
fix: useUserStore 타입스크립트 에러 수정
hyeon9782 Oct 4, 2023
e140ded
fix: TagInput 컴포넌트 에러 수정
hyeon9782 Oct 4, 2023
eb70a6f
feat: my, favorited articles 조회 기능 추가
hyeon9782 Oct 4, 2023
a37fb97
feat: 게시글 작성 기능 구현
hyeon9782 Oct 4, 2023
c6b0550
feat: 좋아요, 좋아요 취소 기능 구현
hyeon9782 Oct 4, 2023
87fbd0a
feat: 프로필 정보 가져오기 기능 구현
hyeon9782 Oct 4, 2023
e9954cb
feat: profile 페이지 기능 구현
hyeon9782 Oct 4, 2023
1933758
feat: Article 수정 페이지 구현
hyeon9782 Oct 4, 2023
d10e622
feat: Article 삭제 기능 구현
hyeon9782 Oct 4, 2023
ef5e2b2
feat: Article 수정 기능 구현
hyeon9782 Oct 4, 2023
6aad78c
feat: 댓글 달기 기능 구현
hyeon9782 Oct 4, 2023
f6cda8e
feat: 댓글 삭제 기능 구현
hyeon9782 Oct 4, 2023
2819603
feat: 회원 정정보 수정 기능 구현
hyeon9782 Oct 4, 2023
aa6dfaf
feat: Follow 기능 구현
hyeon9782 Oct 5, 2023
b1539e1
feat: LoginForm 컴포넌트 useLogin 커스텀 훅 구현
hyeon9782 Oct 5, 2023
518e466
feat & refactor: Login, Register Page 리팩토링 및 useAuth 구현
hyeon9782 Oct 5, 2023
3f5c018
refactor & feat: SettingForm , LogoutButton 컴포넌트 생성
hyeon9782 Oct 7, 2023
e077855
fix: 회원가입 기능 route handler 수정
hyeon9782 Oct 7, 2023
35edc4f
feat & refactor: useProfile 커스텀 훅 구현
hyeon9782 Oct 7, 2023
2a38d12
refactor: favorite, unfavorite 기능 useArticles에 포함
hyeon9782 Oct 7, 2023
a0c45ce
fix: favorite articles에 token값 추가
hyeon9782 Oct 7, 2023
50dcc3e
refactor: ProfileBox 컴포넌트 구현
hyeon9782 Oct 7, 2023
d98a93d
fix: follow 기능 에러 해결
hyeon9782 Oct 8, 2023
33b5c7a
feat & refactor: EditForm으로 수정 페이지와 작성 페이지 수정
hyeon9782 Oct 8, 2023
0720993
fix: ArticlesList 에러 해결
hyeon9782 Oct 8, 2023
ec99a30
feat: Type 세분화 및 적용
hyeon9782 Oct 8, 2023
7f488cc
Update README.md
hyeon9782 Oct 18, 2023
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
57 changes: 0 additions & 57 deletions .eslintrc.js

This file was deleted.

50 changes: 50 additions & 0 deletions .github/workflows/review-assign-action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Review Assign

on:
pull_request:
types: [opened, ready_for_review]

jobs:
assign:
permissions:
actions: write
checks: write
contents: write
deployments: write
discussions: write
issues: write
id-token: read
packages: write
pages: write
pull-requests: write
repository-projects: write
security-events: write
statuses: write
runs-on: ubuntu-latest
steps:
- if: github.base_ref == 'main' # base branch name is 'master'
run: echo REVIEWERS=inseong-so >> $GITHUB_ENV
- if: startsWith(github.base_ref, 'team1')
run: echo REVIEWERS=headring, KimHunJin, hyjoong her0707 >> $GITHUB_ENV
- if: startsWith(github.base_ref, 'team2')
run: echo REVIEWERS=Bsfla, SeolJaeHyeok, choisy9619, kyung-jun >> $GITHUB_ENV
- if: startsWith(github.base_ref, 'team3')
run: echo REVIEWERS=sgsg9447, kingyong9169, 2dowon, jqkk >> $GITHUB_ENV
- if: startsWith(github.base_ref, 'team4')
run: echo REVIEWERS=kimseongchan-kr, cham0287, hyeon9782 >> $GITHUB_ENV
- if: startsWith(github.base_ref, 'team5')
run: echo REVIEWERS=2-NOW, hyew-kim, geeonie >> $GITHUB_ENV
- if: startsWith(github.base_ref, 'team6')
run: echo REVIEWERS=areumsheep, ludacirs, innocarpe >> $GITHUB_ENV
- if: startsWith(github.base_ref, 'team7')
run: echo REVIEWERS=endmoseung, steven-yn, ding-co, mandarin-sep >> $GITHUB_ENV
- if: startsWith(github.base_ref, 'team8')
run: echo REVIEWERS=HOJOON07, jiji-hoon96, 71summernight, seung-wan >> $GITHUB_ENV
- if: startsWith(github.base_ref, 'team9')
run: echo REVIEWERS=Siihyun, hhhminme, 0uizi0, brgndyy >> $GITHUB_ENV
- if: startsWith(github.base_ref, 'team10')
run: echo REVIEWERS=Leejha, steadily-worked >> $GITHUB_ENV
- uses: hkusu/review-assign-action@v1
with:
assignees: ${{ github.actor }}
reviewers: ${{ env.REVIEWERS }}
16 changes: 0 additions & 16 deletions .prettierrc.js

This file was deleted.

75 changes: 65 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,77 @@
# ![RealWorld Example App](./assets/logo.png)
# Next World

> ### [YOUR_FRAMEWORK] codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API.
### Real World에서 제공해주는 API를 활용하여 블로그를 개발한 프로젝트입니다.

## 프로젝트 목표

### [Demo](https://demo.realworld.io/)    [RealWorld](https://github.com/gothinkster/realworld)
### Next.js 13 App Router의 사용법을 익히고 SSR 이해하기

### Vanilla Extract의 사용법을 익히고 제로 런타임 이해하기

This codebase was created to demonstrate a fully fledged fullstack application built with **[YOUR_FRAMEWORK]** including CRUD operations, authentication, routing, pagination, and more.
### React Query의 사용법을 익히고 효율적인 데이터 패칭을 구현하기

We've gone to great lengths to adhere to the **[YOUR_FRAMEWORK]** community styleguides & best practices.
### Zustand의 사용법을 익히고 Flux 패턴 이해하기

For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo.
## Stacks

### Environment

# How it works
<div style="display: flex">
<img src="https://img.shields.io/badge/Visual Studio Code-007ACC?style=for-the-badge&logo=Visual Studio Code&logoColor=white">
<img src="https://img.shields.io/badge/Git-F05032?style=for-the-badge&logo=git&logoColor=white">
<img src="https://img.shields.io/badge/GitHub-181717?style=for-the-badge&logo=GitHub&logoColor=white">
</div>

> Describe the general architecture of your app here
### Config

# Getting started
<img src="https://img.shields.io/badge/Npm-CB3837?style=for-the-badge&logo=npm&logoColor=white">

> npm install, npm start, etc.
### Development

<div style="display: flex">
<img src="https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white">
<img src="https://img.shields.io/badge/Next-000000?style=for-the-badge&logo=next.js&logoColor=white">
<img src="https://img.shields.io/badge/Vanilla Extract-DB7093?style=for-the-badge&logo=vanilla extract&logoColor=white">
<img src="https://img.shields.io/badge/Zustand-3578E5?style=for-the-badge&logo=Zustand&logoColor=white">
<img src="https://img.shields.io/badge/React Query-FF4154?style=for-the-badge&logo=React Query&logoColor=white">
</div>

## 페이지 구성

### 메인 페이지 (Article 목록)

### Article 상세 페이지

### 로그인 페이지

### 회원가입 페이지

### 설정 페이지

### 글쓰기 페이지

### 프로필 페이지

## 주요 기능

- Article CRUD 기능 구현 (전체, 태그, 좋아요, 팔로우)
- Comment CRD 기능 구현
- User & Auth 기능 구현 (로그인, 회원가입, 정보 수정)
- 좋아요 & 팔로우 기능 구현

## Future Works

- [ ] cookies 넣는 부분 util 함수로 빼기
- [ ] route handler Response 일관성 있게 통일하기
- [ ] Error Message에 따라 알맞은 에러 처리
- [ ] 사용하지 않는 함수들 제거하기
- [ ] 좋아요 & 팔로우 버튼
- [ ] Optimistic Updates를 활용한 사용자 경험 향상
- [ ] 일관된 UI를 위해 button 크기 고정 (좋아요 수가 99개가 넘어갈 경우 99+로 표시)
- [ ] ArticlePreview
- [ ] 제목 크기 고정 및 크기를 넘어가면 ... 처리
- [ ] 한 번 봤던 게시글 표시하기 (체크 표시 또는 배경색을 다르게)
- [ ] alert을 사용하지 않고 Dialog 컴포넌트 구현
- [ ] 페이지 별 스켈레톤 UI 적용
- [ ] Vanilla Extract 기능을 활용하여 CSS 정리 (급하게 하느라 너무 막 짠 거 같습니다..)
- [ ] 테스트 코드 추가
43 changes: 43 additions & 0 deletions api/http/httpClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { HTTP_METHOD, COMMON_HEADERS } from '@/constants/api';
import { API_BASE_URL } from '@/constants/env';
Copy link
Member

Choose a reason for hiding this comment

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

환경변수는 상수 파일보다 .env에서 관리하면 좋을 것 같네요! 네이밍에서 그 의도를 드러낸 바와 같이 말이죠


class HttpClient {
BASE_URL = API_BASE_URL;

constructor() {}

Comment on lines +6 to +8
Copy link
Member

Choose a reason for hiding this comment

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

이러면 생성자 함수가 의미가 있을까... 싶습니다!
객체와 클래스의 차이가 무엇일까요?

async request(url: string, options: any, method: string) {
const response = await fetch(`${this.BASE_URL}${url}`, {
Comment on lines +9 to +10
Copy link
Member

Choose a reason for hiding this comment

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

fetch API의 options는 타입 선언 파일에 제공되므로, any 보다는 해당 타입을 확장해서 사용하거나 차용하는 것을 추천드려요.

method,
headers: {
...COMMON_HEADERS,
...options.headers,
},
...options,
});

if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}

return response;
Comment on lines +19 to +23
Copy link
Member

Choose a reason for hiding this comment

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

서버 에러 또는 네트워크 에러일 때 핸들링은 어떻게 될까요?

}

get(url: string, options = {}) {
return this.request(url, options, HTTP_METHOD.GET);
}

post(url: string, options = {}) {
return this.request(url, options, HTTP_METHOD.POST);
}

put(url: string, options = {}) {
return this.request(url, options, HTTP_METHOD.PUT);
}

delete(url: string, options = {}) {
return this.request(url, options, HTTP_METHOD.DELETE);
}
}

export const httpClient = new HttpClient();
35 changes: 0 additions & 35 deletions app/[slug]/page.tsx

This file was deleted.

File renamed without changes.
30 changes: 30 additions & 0 deletions app/[username]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

import ArticleList from '@/components/article/ArticleList';
import ProfileBox from '@/components/profile/ProfileBox';
import useProfile from '@/hooks/useProfile';
import { container } from '@/styles/common.css';
import dynamic from 'next/dynamic';
import { Suspense } from 'react';

const ArticleTab = dynamic(() => import('@/components/article/ArticleTab'), { ssr: false });
type Props = {
params: { username: string };
};
const ProfilePage = ({ params: { username } }: Props) => {
hyeon9782 marked this conversation as resolved.
Show resolved Hide resolved
const { profile } = useProfile({ username });
hyeon9782 marked this conversation as resolved.
Show resolved Hide resolved

return (
<section>
<ProfileBox username={profile.username} following={profile.following} image={profile.image} />
<div className={container}>
<ArticleTab />
<Suspense fallback={<div>리스트 로딩 중...</div>}>
<ArticleList username={profile.username} />
</Suspense>
</div>
</section>
);
};

export default ProfilePage;
60 changes: 60 additions & 0 deletions app/api/articles/[slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { http } from '@/utils/http';
import { NextRequest, NextResponse } from 'next/server';

async function GET(req: NextRequest, route: { params: { slug: string } }) {
try {
const slug = route.params.slug;
Copy link
Member

Choose a reason for hiding this comment

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

위에 구조분해 한 걸 사용하면 안되나요?

const token = req.cookies.get('token')?.value || '';

const res = await http.get(`/articles/${slug}`, {
headers: {
'Content-Type': 'application/json; charset=utf-8',
Comment on lines +10 to +11
Copy link
Member

Choose a reason for hiding this comment

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

이 헤더는 default 값으로 선언해줘도 좋을 것 같아요

Authorization: `Token ${token}`,
},
});

return NextResponse.json({ message: 'Article Get Success', success: true, data: res });
} catch (error: any) {
console.log(error);
Comment on lines +17 to +18
Copy link
Member

Choose a reason for hiding this comment

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

error도 unknown 타입으로 핸들링하는 함수를 만들어주면 진짜 요긴하게 쓸 수 있답니다!

return NextResponse.json({ error: error.message }, { status: 400 });
}
}

async function PUT(req: NextRequest, route: { params: { slug: string } }) {
try {
const body = await req.json();
const slug = route.params.slug;
const token = req.cookies.get('token')?.value || '';

const res = await http.put(`/articles/${slug}`, body, {
headers: {
'Content-Type': 'application/json; charset=utf-8',
Authorization: `Token ${token}`,
},
});

return NextResponse.json({ message: 'Article Update Success', success: true, data: res });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
}

async function DELETE(req: NextRequest, route: { params: { slug: string } }) {
try {
const slug = route.params.slug;
const token = req.cookies.get('token')?.value || '';

const res = await http.delete(`/articles/${slug}`, {
headers: {
'Content-Type': 'application/json; charset=utf-8',
Authorization: `Token ${token}`,
},
});

return NextResponse.json({ message: 'Article Delete Success', success: true, data: res });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
}

export { GET, PUT, DELETE };
Loading