Skip to content

Commit

Permalink
Merge pull request #155 from hyeon9782/main
Browse files Browse the repository at this point in the history
feat: Next / TypeScript / React Query / Zustand / Vanilla Extract를 사용하여 Real World 1차 기능 구현을 완료합니다.
  • Loading branch information
InSeong-So authored May 16, 2024
2 parents a8c1b37 + 7f488cc commit 01f8117
Show file tree
Hide file tree
Showing 89 changed files with 1,964 additions and 997 deletions.
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';

class HttpClient {
BASE_URL = API_BASE_URL;

constructor() {}

async request(url: string, options: any, method: string) {
const response = await fetch(`${this.BASE_URL}${url}`, {
method,
headers: {
...COMMON_HEADERS,
...options.headers,
},
...options,
});

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

return response;
}

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) => {
const { profile } = useProfile({ username });

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;
const token = req.cookies.get('token')?.value || '';

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

return NextResponse.json({ message: 'Article Get Success', success: true, data: res });
} catch (error: any) {
console.log(error);
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

0 comments on commit 01f8117

Please sign in to comment.