-
Notifications
You must be signed in to change notification settings - Fork 41
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
feat: Next / TypeScript / React Query / Zustand / Vanilla Extract를 사용하여 Real World 1차 기능 구현을 완료합니다. #155
Changes from all commits
f59fd0f
4d80c9f
e577cd8
4bedb3a
6ea21d9
daad074
dab9319
7681666
9419f91
d1f6b43
89b8039
50f225e
fc4f103
32bceb4
1dc77fb
209d8ab
04c1062
77d9718
6f9e355
d47e250
b7d3495
12f44e4
be02273
a761449
e140ded
eb70a6f
a37fb97
c6b0550
87fbd0a
e9954cb
1933758
d10e622
ef5e2b2
6aad78c
f6cda8e
2819603
aa6dfaf
b1539e1
518e466
3f5c018
e077855
35edc4f
2a38d12
a0c45ce
50dcc3e
d98a93d
33b5c7a
0720993
ec99a30
7f488cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
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 }} |
This file was deleted.
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 정리 (급하게 하느라 너무 막 짠 거 같습니다..) | ||
- [ ] 테스트 코드 추가 |
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'; | ||
|
||
class HttpClient { | ||
BASE_URL = API_BASE_URL; | ||
|
||
constructor() {} | ||
|
||
Comment on lines
+6
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); |
This file was deleted.
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; |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
환경변수는 상수 파일보다
.env
에서 관리하면 좋을 것 같네요! 네이밍에서 그 의도를 드러낸 바와 같이 말이죠