From f59fd0f7d5662531268e70d3d7051ccfae119934 Mon Sep 17 00:00:00 2001 From: InSeong-So Date: Sat, 2 Sep 2023 17:29:53 +0900 Subject: [PATCH 01/49] =?UTF-8?q?feat:=20review-assign-action.yml=20?= =?UTF-8?q?=EA=B9=83=ED=97=99=20=EC=95=A1=EC=85=98=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/review-assign-action.yml | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/review-assign-action.yml diff --git a/.github/workflows/review-assign-action.yml b/.github/workflows/review-assign-action.yml new file mode 100644 index 00000000..518308b5 --- /dev/null +++ b/.github/workflows/review-assign-action.yml @@ -0,0 +1,36 @@ +name: Review Assign + +on: + pull_request: + types: [opened, ready_for_review] + +jobs: + assign: + 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 }} From 4d80c9f2334589f6086c7662b4b8737c4c2dddb2 Mon Sep 17 00:00:00 2001 From: InSeong-So Date: Thu, 7 Sep 2023 20:54:37 +0900 Subject: [PATCH 02/49] Update review-assign-action.yml --- .github/workflows/review-assign-action.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/review-assign-action.yml b/.github/workflows/review-assign-action.yml index 518308b5..4b421c09 100644 --- a/.github/workflows/review-assign-action.yml +++ b/.github/workflows/review-assign-action.yml @@ -6,6 +6,9 @@ on: jobs: assign: + permissions: + packages: write + pull-requests: write runs-on: ubuntu-latest steps: - if: github.base_ref == 'main' # base branch name is 'master' From e577cd859df1e68fec7738c4f8d507f5a691bb51 Mon Sep 17 00:00:00 2001 From: InSeong-So Date: Thu, 7 Sep 2023 20:56:45 +0900 Subject: [PATCH 03/49] Update review-assign-action.yml --- .github/workflows/review-assign-action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/review-assign-action.yml b/.github/workflows/review-assign-action.yml index 4b421c09..beb6df61 100644 --- a/.github/workflows/review-assign-action.yml +++ b/.github/workflows/review-assign-action.yml @@ -7,7 +7,7 @@ on: jobs: assign: permissions: - packages: write + contents: read pull-requests: write runs-on: ubuntu-latest steps: From 4bedb3ac32907cdbab0e74fd0b90fd2774e56efd Mon Sep 17 00:00:00 2001 From: InSeong-So Date: Thu, 7 Sep 2023 21:22:33 +0900 Subject: [PATCH 04/49] Update review-assign-action.yml --- .github/workflows/review-assign-action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/review-assign-action.yml b/.github/workflows/review-assign-action.yml index beb6df61..518b5ffd 100644 --- a/.github/workflows/review-assign-action.yml +++ b/.github/workflows/review-assign-action.yml @@ -7,7 +7,7 @@ on: jobs: assign: permissions: - contents: read + contents: write pull-requests: write runs-on: ubuntu-latest steps: From 6ea21d9905930744ba6944344800e01cb74eba39 Mon Sep 17 00:00:00 2001 From: InSeong-So Date: Thu, 7 Sep 2023 21:22:49 +0900 Subject: [PATCH 05/49] Update review-assign-action.yml --- .github/workflows/review-assign-action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/review-assign-action.yml b/.github/workflows/review-assign-action.yml index 518b5ffd..b55e7e80 100644 --- a/.github/workflows/review-assign-action.yml +++ b/.github/workflows/review-assign-action.yml @@ -7,8 +7,8 @@ on: jobs: assign: permissions: - contents: write - pull-requests: write + contents: read + pull-requests: read runs-on: ubuntu-latest steps: - if: github.base_ref == 'main' # base branch name is 'master' From daad07472e6a100a528b6f73a3758d761ba1326b Mon Sep 17 00:00:00 2001 From: InSeong-So Date: Thu, 7 Sep 2023 21:56:56 +0900 Subject: [PATCH 06/49] Update review-assign-action.yml --- .github/workflows/review-assign-action.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/review-assign-action.yml b/.github/workflows/review-assign-action.yml index b55e7e80..21608d55 100644 --- a/.github/workflows/review-assign-action.yml +++ b/.github/workflows/review-assign-action.yml @@ -7,8 +7,19 @@ on: jobs: assign: permissions: - contents: read - pull-requests: read + 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' From dab9319fb3aaf5f89fed86747c98648d7060ca4b Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Thu, 14 Sep 2023 22:06:03 +0900 Subject: [PATCH 07/49] feat: useInfiniteQuery test --- .eslintrc.js | 57 ---------------------------- .prettierrc.js | 16 -------- components/article/ArticleList.tsx | 61 ++++++++++++------------------ 3 files changed, 25 insertions(+), 109 deletions(-) delete mode 100644 .eslintrc.js delete mode 100644 .prettierrc.js diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index d5dd3b78..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,57 +0,0 @@ -module.exports = { - extends: ["eslint:recommended"], - plugins: ["@typescript-eslint"], - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaVersion: 12, - sourceType: "module", - ecmaFeatures: { - jsx: true, - }, - }, - env: { - es6: true, - browser: true, - node: true, - }, - rules: { - indent: "off", - "brace-style": "off", - "arrow-parens": "off", - "no-console": "off", - "no-undef": "off", - "max-len": "off", - "sort-imports": "off", - "no-restricted-exports": "off", - "no-unused-vars": "off", - "object-curly-newline": "off", - "max-params": ["error", 3], - "jsx-quotes": "off", - "no-confusing-arrow": "off", - "no-nested-ternary": "off", - "comma-spacing": "off", - "function-paren-newline": "off", - "implicit-arrow-linebreak": "off", - "operator-linebreak": "off", - "no-underscore-dangle": "off", - "no-useless-constructor": "off", - "no-use-before-define": "off", - "no-param-reassign": "off", - "no-return-await": "off", - "prefer-regex-literals": "off", - "lines-between-class-members": "off", - "import/no-unresolved": "off", - // typescript - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/no-unused-vars": "error", - "@typescript-eslint/restrict-template-expressions": "off", - "@typescript-eslint/no-var-requires": "error", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/ban-ts-comment": "off", - }, -}; diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 57294e79..00000000 --- a/.prettierrc.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - plugins: [require("@trivago/prettier-plugin-sort-imports")], - singleQuote: true, - semi: true, - useTabs: false, - tabWidth: 2, - trailingComma: "all", - printWidth: 100, - bracketSpacing: true, - arrowParens: "always", - endOfLine: "auto", - importOrder: ["^@/*", "^./(.*)", "^types", "^public"], - importOrderSortSpecifiers: true, - importOrderGroupNamespaceSpecifiers: true, - importOrderCaseInsensitive: true, -}; diff --git a/components/article/ArticleList.tsx b/components/article/ArticleList.tsx index 675cf3ed..11034d34 100644 --- a/components/article/ArticleList.tsx +++ b/components/article/ArticleList.tsx @@ -1,44 +1,33 @@ -// 'use client'; -import { ArticleAPI, fetchArticles } from '@/services/articles'; +'use client'; +import { ArticleAPI } from '@/services/articles'; import ArticlePreview from './ArticlePreview'; import { useInfiniteQuery } from '@tanstack/react-query'; -import React, { useRef } from 'react'; +import React from 'react'; -const ArticleList = async () => { - const { articles } = await ArticleAPI.all(1); +const ArticleList = () => { + const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status } = useInfiniteQuery({ + queryKey: ['articles'], + queryFn: ({ pageParam = 1 }) => ArticleAPI.all(pageParam), + getNextPageParam: (lastPage, pages) => { + return true; + }, + retry: false, + }); - // const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status } = useInfiniteQuery({ - // queryKey: ['articles'], - // queryFn: ({ pageParam = 1 }) => ArticleAPI.all(pageParam), - // getNextPageParam: (lastPage, pages) => { - // if (5 > page.current) { - // return page.current; - // } - // return undefined; - // }, - // retry: false, - // onSuccess: () => { - // console.log('성공' + page.current); + return ( +
+ + {data?.pages.map((group, i) => ( +
+ {group.articles.map(article => ( + + ))} +
+ ))} +
+ ); - // page.current++; - // }, - // onError: () => {}, - // }); - - // return ( - //
- // - // {data?.pages.map((group, i) => ( - //
- // {group.articles.map(article => ( - // - // ))} - //
- // ))} - //
- // ); - - return
{articles?.map(article => )}
; + // return
{articles?.map(article => )}
; }; export default ArticleList; From 9419f9158d1bb07e7a12668bbd98385cd3924a1e Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Fri, 15 Sep 2023 00:52:48 +0900 Subject: [PATCH 08/49] =?UTF-8?q?refactor=20&=20feat:=20=ED=9A=A8=EC=9C=A8?= =?UTF-8?q?=EC=A0=81=EC=9D=B8=20fetch=20=EC=82=AC=EC=9A=A9=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20fetch=20=EB=9E=98=ED=95=91=20=EB=B0=8F=20a?= =?UTF-8?q?xios=EC=97=90=EC=84=9C=20fetch=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/article/ArticleList.tsx | 11 ++- components/article/ArticlePreview.tsx | 13 ++-- components/layouts/SideBar.tsx | 4 +- libs/http.ts | 27 -------- services/articles.ts | 97 +++------------------------ services/comments.ts | 8 +-- services/favorites.ts | 43 ------------ services/profile.ts | 8 +-- services/users.ts | 61 +++-------------- utils/http.ts | 88 ++++++++++++++++++++++++ 10 files changed, 132 insertions(+), 228 deletions(-) delete mode 100644 libs/http.ts delete mode 100644 services/favorites.ts create mode 100644 utils/http.ts diff --git a/components/article/ArticleList.tsx b/components/article/ArticleList.tsx index 11034d34..776cb37c 100644 --- a/components/article/ArticleList.tsx +++ b/components/article/ArticleList.tsx @@ -1,13 +1,14 @@ 'use client'; -import { ArticleAPI } from '@/services/articles'; +import { getArticlesAPI } from '@/services/articles'; import ArticlePreview from './ArticlePreview'; import { useInfiniteQuery } from '@tanstack/react-query'; import React from 'react'; +import { flexCenter, greenButton } from '@/styles/common.css'; const ArticleList = () => { const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status } = useInfiniteQuery({ queryKey: ['articles'], - queryFn: ({ pageParam = 1 }) => ArticleAPI.all(pageParam), + queryFn: ({ pageParam = 1 }) => getArticlesAPI(pageParam), getNextPageParam: (lastPage, pages) => { return true; }, @@ -16,7 +17,6 @@ const ArticleList = () => { return (
- {data?.pages.map((group, i) => (
{group.articles.map(article => ( @@ -24,6 +24,11 @@ const ArticleList = () => { ))}
))} +
+ +
); diff --git a/components/article/ArticlePreview.tsx b/components/article/ArticlePreview.tsx index fe220c37..a7a7a7f0 100644 --- a/components/article/ArticlePreview.tsx +++ b/components/article/ArticlePreview.tsx @@ -5,8 +5,9 @@ import TagList from '../tags/TagList'; import { useRouter } from 'next/navigation'; import { FillHeartIcon } from '@/composables/icons'; import { fillGreenButton, flex, flexBetween, greenButton } from '@/styles/common.css'; -import { useMutation } from '@tanstack/react-query'; -import { favoriteArticleAPI, unFavoriteArticleAPI } from '@/services/favorites'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { favoriteAPI, unFavoriteAPI } from '@/services/articles'; + type Props = { article: any; }; @@ -15,15 +16,15 @@ const ArticlePreview = ({ }: Props) => { const router = useRouter(); + const queryClient = useQueryClient(); + const { mutate } = useMutation({ - mutationFn: favorited ? unFavoriteArticleAPI : favoriteArticleAPI, + mutationFn: favorited ? favoriteAPI : unFavoriteAPI, onError: err => { console.error(err); }, onSuccess: res => { - console.log(res); - - console.log('좋아요!'); + queryClient.invalidateQueries({ queryKey: ['articles'] }); }, }); return ( diff --git a/components/layouts/SideBar.tsx b/components/layouts/SideBar.tsx index 914ef61a..c0e48a54 100644 --- a/components/layouts/SideBar.tsx +++ b/components/layouts/SideBar.tsx @@ -2,10 +2,10 @@ import React from 'react'; import TagList from '../tags/TagList'; import { sideBar, sideBarText } from '@/styles/layout.css'; import { sidePadding } from '@/styles/common.css'; -import { http } from '@/libs/http'; +import { http } from '@/utils/http'; const SideBar = async () => { - const { tags } = await http.get('https://api.realworld.io/api/tags'); + const { tags } = await http.get('/tags'); return (
diff --git a/libs/http.ts b/libs/http.ts deleted file mode 100644 index 8fffadf8..00000000 --- a/libs/http.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Axios from 'axios'; - -const axios = Axios.create(); - -export const http = { - get: function get(url: string) { - return axios.get(url).then(res => res.data); - }, - post: function post(url: string, body?: Request) { - return axios - .post(url, body) - .then(res => { - console.log('성공'); - - return res.data; - }) - .catch(err => { - console.log('에러 발생'); - }); - }, - put: function put(url: string, body?: Request) { - return axios.put(url, body).then(res => res.data); - }, - delete: function remove(url: string) { - return axios.delete(url).then(res => res.data); - }, -}; diff --git a/services/articles.ts b/services/articles.ts index e2f3bf45..5f6cdd5c 100644 --- a/services/articles.ts +++ b/services/articles.ts @@ -1,96 +1,19 @@ -import { API_BASE_URL } from '@/constants/env'; -import { http } from '@/libs/http'; -import { Article, NewArticle } from '@/types'; +import { http } from '@/utils/http'; -const ArticleAPI = { - all: async (offset?: number, limit = 10) => { - return fetch(`${API_BASE_URL}/articles?limit=${limit}&offset=${offset ? offset * limit : 0}`, { - method: 'GET', - headers: { 'Content-Type': 'application/json; charset=utf-8' }, - }).then(res => { - if (!(res.status === 200)) { - throw new Error('Error'); - } - return res.json(); - }); - }, - byTag: async (tag: string, offset = 0, limit = 10) => { - return fetch(`${API_BASE_URL}/articles?tag=${tag}&limit=${limit}&offset=${offset ? offset * limit : 0}`, { - method: 'GET', - headers: { 'Content-Type': 'application/json; charset=utf-8' }, - }).then(res => { - if (!(res.status === 200)) { - throw new Error('Error'); - } - return res.json(); - }); - }, - favorite: (slug: string) => { - return fetch(`${API_BASE_URL}/articles/${slug}/favorite`, { - method: 'POST', - headers: { 'Content-Type': 'application/json; charset=utf-8' }, - }).then(res => { - if (!(res.status === 201)) { - throw new Error('Error'); - } - return res.json(); - }); - }, - unFavorite: (slug: string) => { - return fetch(`${API_BASE_URL}/articles/${slug}/favorite`, { - method: 'DELETE', - headers: { 'Content-Type': 'application/json; charset=utf-8' }, - }).then(res => { - if (!(res.status === 201)) { - throw new Error('Error'); - } - return res.json(); - }); - }, +const getArticlesAPI = (offset = 0, limit = 10) => { + return http.get(`/articles?limit=${limit}&offset=${offset ? offset * limit : 0}`); }; -// 전제 Article 조회 -const fetchArticles = (limit = 20) => { - return http.get(`https://api.realworld.io/api/articles?limit=${limit}`); +const getArticlesWithTagAPI = (tag: string, offset = 0, limit = 10) => { + return http.get(`/articles?tag=${tag}&limit=${limit}&offset=${offset ? offset * limit : 0}`); }; -// Tag로 Article 조회 -const fetchArticlesWithTag = (tag: string, limit = 20) => { - return http.get(`https://api.realworld.io/api/articles?limit=${limit}&tag=${tag}`); +const favoriteAPI = (slug: string) => { + return http.post(`/articles/${slug}/favorite`); }; -// Follow한 user의 Article 조회 -const fetchFollowArticles = async () => { - return await http.get('https://api.realworld.io/api/articles/feed'); +const unFavoriteAPI = (slug: string) => { + return http.delete(`/articles/${slug}/favorite`); }; -// Article 작성 -const registerArticle = (article: NewArticle) => { - return http.post('https://api.realworld.io/api/articles', article); -}; - -// Article 단건 조회 -const fetchArticle = async (slug: string): Promise
=> { - return http.get(`https://api.realworld.io/api/articles/${slug}`).then(res => res.article); -}; - -// Article 수정 -const updateArticle = (slug: string) => { - return http.put(`https://api.realworld.io/api/articles/${slug}`); -}; - -// Article 삭제 -const deleteArticle = (slug: string) => { - return http.delete(`https://api.realworld.io/api/articles/${slug}`); -}; - -export { - fetchArticles, - fetchArticlesWithTag, - fetchFollowArticles, - fetchArticle, - registerArticle, - updateArticle, - deleteArticle, - ArticleAPI, -}; +export { getArticlesAPI, getArticlesWithTagAPI, favoriteAPI, unFavoriteAPI }; diff --git a/services/comments.ts b/services/comments.ts index 79fad89f..9885f3dd 100644 --- a/services/comments.ts +++ b/services/comments.ts @@ -1,15 +1,15 @@ -import { http } from '@/libs/http'; +import { http } from '@/utils/http'; const getComments = (slug: string) => { - return http.get(`https://api.realworld.io/api/articles/${slug}/comments`); + return http.get(`/articles/${slug}/comments`); }; const createComment = (slug: string) => { - return http.post(`https://api.realworld.io/api/articles/${slug}/comments`); + return http.post(`/articles/${slug}/comments`); }; const deleteComment = (slug: string, id: string) => { - return http.delete(`https://api.realworld.io/api/articles/${slug}/comments/${id}`); + return http.delete(`/articles/${slug}/comments/${id}`); }; export { getComments, createComment, deleteComment }; diff --git a/services/favorites.ts b/services/favorites.ts deleted file mode 100644 index ebbcb060..00000000 --- a/services/favorites.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { API_BASE_URL } from '@/constants/env'; -import { getCookie } from '@/utils/cookies'; - -// Article 좋아요 -const favoriteArticleAPI = async (slug: string) => { - const accessToken = getCookie('token'); - console.log(accessToken); - - return fetch( - 'https://api.realworld.io/api/articles/Try-to-generate-the-TCP-bus-maybe-it-will-override-the-neural-bandwidth%21-120863/favorite', - { - method: 'POST', - headers: { - accept: 'application/json', - // 'Content-Type': 'application/json; charset=utf-8', - Authorization: `Token ${accessToken}`, - }, - } - ).then(res => { - if (!(res.status === 200)) { - throw new Error('Error'); - } - return res.json(); - }); -}; - -// Article 좋아요 취소 -const unFavoriteArticleAPI = async (slug: string) => { - const accessToken = getCookie('token'); - console.log(accessToken); - - return fetch(`${API_BASE_URL}/articles/${slug}/favorite`, { - method: 'DELETE', - headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: `Token ${accessToken}` }, - }).then(res => { - if (!(res.status === 200)) { - throw new Error('Error'); - } - return res.json(); - }); -}; - -export { favoriteArticleAPI, unFavoriteArticleAPI }; diff --git a/services/profile.ts b/services/profile.ts index 406e782e..e4a0e4d9 100644 --- a/services/profile.ts +++ b/services/profile.ts @@ -1,15 +1,15 @@ -import { http } from '@/libs/http'; +import { http } from '@/utils/http'; const getProfile = (username: string) => { - return http.get(`https://api.realworld.io/api/profiles/${username}`); + return http.get(`/profiles/${username}`); }; const followUser = (username: string) => { - return http.post(`https://api.realworld.io/api/profiles/${username}`); + return http.post(`/profiles/${username}`); }; const unFollowUser = (username: string) => { - return http.delete(`https://api.realworld.io/api/profiles/${username}`); + return http.delete(`/profiles/${username}`); }; export { getProfile, followUser, unFollowUser }; diff --git a/services/users.ts b/services/users.ts index f3daa38b..b3a699d7 100644 --- a/services/users.ts +++ b/services/users.ts @@ -1,67 +1,24 @@ -import { API_BASE_URL } from '@/constants/env'; import { LoginUser, NewUser, UpdateUser } from '@/types'; -import { getCookie } from '@/utils/cookies'; -import Error from 'next/error'; +import { http } from '@/utils/http'; -// Register a new user +// 회원가입 const registerUserAPI = async (user: NewUser) => { - console.log(user); - - return fetch(`${API_BASE_URL}/users`, { - method: 'POST', - headers: { 'Content-Type': 'application/json; charset=utf-8' }, - body: JSON.stringify({ user }), - }).then(res => { - if (!(res.status === 201)) { - throw new Error('Error'); - } - return res.json(); - }); + return http.post('/users', { user }); }; -// Login for existing user +// 로그인 const loginAPI = async (user: LoginUser) => { - return fetch(`${API_BASE_URL}/users/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json; charset=utf-8' }, - body: JSON.stringify({ user }), - }).then(res => { - if (!(res.status === 200)) { - throw new Error('Error'); - } - return res.json(); - }); + return http.post('/users/login', { user }); }; -// Updated user information for current user +// 회원정보 수정 const updateUserAPI = async (user: UpdateUser) => { - const accessToken = getCookie('token'); - console.log(accessToken); - - return fetch(`${API_BASE_URL}/user`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: `Token ${accessToken}` }, - body: JSON.stringify({ user }), - }).then(res => { - if (!(res.status === 200)) { - throw new Error('Error'); - } - return res.json(); - }); + return http.put('/user', { user }); }; -// Gets the currently logged-in user +// 현재 유저 조회 const getUserAPI = async () => { - const accessToken = getCookie('token'); - return fetch(`${API_BASE_URL}/user`, { - method: 'GET', - headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: `Token ${accessToken}` }, - }).then(res => { - if (!(res.status === 200)) { - throw new Error('Error'); - } - return res.json(); - }); + return http.get('/user'); }; export { registerUserAPI, loginAPI, getUserAPI, updateUserAPI }; diff --git a/utils/http.ts b/utils/http.ts new file mode 100644 index 00000000..1760ab8e --- /dev/null +++ b/utils/http.ts @@ -0,0 +1,88 @@ +import { API_BASE_URL } from '@/constants/env'; + +export const http = { + request: async (url: string, method: string, body?: Request, options?: any) => { + const defaultOptions = { + method: method, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: body ? JSON.stringify(body) : undefined, + ...options, + }; + + try { + const response = await fetch(`${API_BASE_URL}${url}`, defaultOptions); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Request failed'); + } + + return response.json(); + } catch (error) { + console.error(error); + throw new Error('Request failed'); + } + }, + + get: (url: string, options?: any) => http.request(url, 'GET', undefined, options), + post: (url: string, body?: any, options?: any) => http.request(url, 'POST', body, options), + put: (url: string, body?: any, options?: any) => http.request(url, 'PUT', body, options), + delete: (url: string, options?: any) => http.request(url, 'DELETE', undefined, options), +}; + +// export const http = { +// get: (url: string, options?: any) => { +// return fetch(`${API_BASE_URL}${url}`, { ...options, method: 'GET' }) +// .then(res => { +// if (!(res.status === 200 || res.status === 201)) { +// throw new Error('Error!'); +// } +// return res.json(); +// }) +// .catch(err => { +// console.error(err); +// throw new Error('Error!'); +// }); +// }, +// post: (url: string, body?: Request, options?: any) => { +// return fetch(`${API_BASE_URL}${url}`, { ...options, method: 'POST', body: JSON.stringify(body) }) +// .then(res => { +// if (!(res.status === 200 || res.status === 201)) { +// throw new Error('Error!'); +// } +// return res.json(); +// }) +// .catch(err => { +// console.error(err); +// throw new Error('Error!'); +// }); +// }, +// put: (url: string, body?: Request, options?: any) => { +// return fetch(`${API_BASE_URL}${url}`, { ...options, method: 'PUT', body: JSON.stringify(body) }) +// .then(res => { +// if (!(res.status === 200 || res.status === 201)) { +// throw new Error('Error!'); +// } +// return res.json(); +// }) +// .catch(err => { +// console.error(err); +// throw new Error('Error!'); +// }); +// }, +// delete: (url: string, options?: any) => { +// return fetch(`${API_BASE_URL}${url}`, { ...options, method: 'DELETE' }) +// .then(res => { +// if (!(res.status === 200 || res.status === 201)) { +// throw new Error('Error!'); +// } +// return res.json(); +// }) +// .catch(err => { +// console.error(err); +// throw new Error('Error!'); +// }); +// }, +// }; From d1f6b43dfd8cc720474b16bfea6d9e2360dcd15b Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Fri, 15 Sep 2023 23:11:59 +0900 Subject: [PATCH 09/49] =?UTF-8?q?feat:=20ArticleList=20useIntersectionObse?= =?UTF-8?q?rver=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=EC=9D=84=20?= =?UTF-8?q?=ED=86=B5=ED=95=9C=20=EB=AC=B4=ED=95=9C=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/user/route.ts | 18 +++++++++++++ app/api/users/route.ts | 0 app/settings/page.tsx | 6 ++++- components/article/ArticleList.tsx | 43 +++++++++++++++++++++++------- hooks/useIntersectionObserver.ts | 26 ++++++++++++++++++ middleware.ts | 8 +++--- services/users.ts | 24 ++++++++++++++--- types/index.ts | 8 +++--- utils/http.ts | 12 +++++++-- 9 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 app/api/user/route.ts delete mode 100644 app/api/users/route.ts create mode 100644 hooks/useIntersectionObserver.ts diff --git a/app/api/user/route.ts b/app/api/user/route.ts new file mode 100644 index 00000000..f6185102 --- /dev/null +++ b/app/api/user/route.ts @@ -0,0 +1,18 @@ +import { NextRequest } from 'next/server'; +import { getUserAPI, updateUserAPI } from '@/services/users'; + +export async function GET(req: NextRequest) { + const { value } = req.cookies.get('token'); + console.log(value); + console.log('route user get'); + + return getUserAPI(value); +} + +export async function PUT(req: NextRequest) { + const { value } = req.cookies.get('token'); + const user = await req.json(); + console.log(user); + console.log('route user put'); + return updateUserAPI(user, value); +} diff --git a/app/api/users/route.ts b/app/api/users/route.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/app/settings/page.tsx b/app/settings/page.tsx index ed259661..c583af52 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -4,6 +4,7 @@ import useUserStore from '@/stores/useUserStore'; import { articleTextarea } from '@/styles/article.css'; import { container, flex, hr, input } from '@/styles/common.css'; import { logoutButton, settingBlock, settingForm, settingTitle, updateButton } from '@/styles/settings.css'; +import { User, UserAction } from '@/types'; import { useMutation } from '@tanstack/react-query'; import { useState } from 'react'; @@ -14,7 +15,7 @@ const SettingsPage = () => { const [formData, setFormData] = useState({ image, username, - bio, + bio: bio ? bio : '', email, password, }); @@ -25,6 +26,8 @@ const SettingsPage = () => { }, onSuccess: res => { alert('회원 정보를 변경했습니다!'); + console.log(res); + updateUser({ ...res.user, }); @@ -100,6 +103,7 @@ const SettingsPage = () => {
+ diff --git a/components/article/ArticleList.tsx b/components/article/ArticleList.tsx index 776cb37c..f15f9721 100644 --- a/components/article/ArticleList.tsx +++ b/components/article/ArticleList.tsx @@ -2,19 +2,48 @@ import { getArticlesAPI } from '@/services/articles'; import ArticlePreview from './ArticlePreview'; import { useInfiniteQuery } from '@tanstack/react-query'; -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { flexCenter, greenButton } from '@/styles/common.css'; +import useIntersectionObserver from '@/hooks/useIntersectionObserver'; const ArticleList = () => { + const targetRef = useRef(null); + const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status } = useInfiniteQuery({ queryKey: ['articles'], - queryFn: ({ pageParam = 1 }) => getArticlesAPI(pageParam), + queryFn: ({ pageParam = 1 }) => { + console.log(pageParam); + + return getArticlesAPI(pageParam); + }, getNextPageParam: (lastPage, pages) => { - return true; + console.log('getNextPageParam 발동'); + + const totalPage = Math.ceil(lastPage.articlesCount / 10); + let currentPage = pages.length; + if (totalPage < pages.length) { + console.log('그만'); + + return undefined; + } + // console.log(totalPage); + // console.log(currentPage); + + return currentPage++; }, retry: false, + refetchOnWindowFocus: false, }); + const nextPage = () => { + if (!hasNextPage || isFetchingNextPage) { + return; + } + fetchNextPage(); + }; + + useIntersectionObserver(nextPage, targetRef); + return (
{data?.pages.map((group, i) => ( @@ -24,15 +53,9 @@ const ArticleList = () => { ))}
))} -
- -
+
); - - // return
{articles?.map(article => )}
; }; export default ArticleList; diff --git a/hooks/useIntersectionObserver.ts b/hooks/useIntersectionObserver.ts new file mode 100644 index 00000000..95947f9f --- /dev/null +++ b/hooks/useIntersectionObserver.ts @@ -0,0 +1,26 @@ +import { RefObject, useEffect } from 'react'; + +const useIntersectionObserver = (cb: () => void, ref: RefObject) => { + useEffect(() => { + const observer = new IntersectionObserver( + entries => { + entries.forEach(entry => { + if (!entry.isIntersecting) return; + cb(); + }); + }, + { threshold: 0.4 } //40%가 보일때를 기본 값으로 설정 했습니다. + ); + if (ref.current) { + observer.observe(ref.current); + } + + return () => { + if (ref.current) { + observer.unobserve(ref.current); + } + }; + }, [cb, ref]); +}; + +export default useIntersectionObserver; diff --git a/middleware.ts b/middleware.ts index 18d275d8..cd389e58 100644 --- a/middleware.ts +++ b/middleware.ts @@ -4,10 +4,10 @@ export async function middleware(req: NextRequest) { const token = true; if (!token) { - // 아직 보류 - if (req.nextUrl.pathname.startsWith('/api')) { - return new NextResponse('Authentiction Error', { status: 401 }); - } + // // 아직 보류 + // if (req.nextUrl.pathname.startsWith('/api')) { + // return new NextResponse('Authentiction Error', { status: 401 }); + // } // 권한 문제 return NextResponse.redirect('http://localhost:3000/login'); } diff --git a/services/users.ts b/services/users.ts index b3a699d7..8226a438 100644 --- a/services/users.ts +++ b/services/users.ts @@ -12,13 +12,29 @@ const loginAPI = async (user: LoginUser) => { }; // 회원정보 수정 -const updateUserAPI = async (user: UpdateUser) => { - return http.put('/user', { user }); +const updateUserAPI = async (user: UpdateUser, auth: string) => { + return http.put( + '/user', + { user }, + { + headers: { + Authorization: `Token ${auth}`, + }, + } + ); }; // 현재 유저 조회 -const getUserAPI = async () => { - return http.get('/user'); +const getUserAPI = async (auth: string) => { + console.log('getUserAPI'); + console.log(auth); + + return http.get('/user', { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Token ${auth}`, + }, + }); }; export { registerUserAPI, loginAPI, getUserAPI, updateUserAPI }; diff --git a/types/index.ts b/types/index.ts index bfa5289c..47a64d9d 100644 --- a/types/index.ts +++ b/types/index.ts @@ -81,10 +81,10 @@ export type Comment = { }; export type UserAction = { - login: (e: any) => void; - updateUser: () => void; - logout: () => void; - reset: () => void; + login?: (e: any) => void; + updateUser?: () => void; + logout?: () => void; + reset?: () => void; }; export type UserResponse = { diff --git a/utils/http.ts b/utils/http.ts index 1760ab8e..66ad06b3 100644 --- a/utils/http.ts +++ b/utils/http.ts @@ -1,5 +1,4 @@ import { API_BASE_URL } from '@/constants/env'; - export const http = { request: async (url: string, method: string, body?: Request, options?: any) => { const defaultOptions = { @@ -10,19 +9,28 @@ export const http = { body: body ? JSON.stringify(body) : undefined, ...options, }; + // console.log('서버자나'); + console.log(API_BASE_URL); + + // console.log(defaultOptions); try { const response = await fetch(`${API_BASE_URL}${url}`, defaultOptions); if (!response.ok) { const errorData = await response.json(); + // console.log('error'); + // console.log(errorData); + throw new Error(errorData.message || 'Request failed'); } + // console.log(response); + return response.json(); } catch (error) { console.error(error); - throw new Error('Request failed'); + // throw new Error('Request failed'); } }, From 89b803981de038a7a7e48788e6c67d53859c57c4 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Fri, 15 Sep 2023 23:46:04 +0900 Subject: [PATCH 10/49] =?UTF-8?q?refactor:=20=EB=AC=B4=ED=95=9C=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EA=B8=B0=EB=8A=A5=20useInfiniteS?= =?UTF-8?q?croll=EB=A1=9C=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/article/ArticleList.tsx | 42 +++------------------------- hooks/useInfiniteScroll.ts | 44 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 38 deletions(-) create mode 100644 hooks/useInfiniteScroll.ts diff --git a/components/article/ArticleList.tsx b/components/article/ArticleList.tsx index f15f9721..99cf2be1 100644 --- a/components/article/ArticleList.tsx +++ b/components/article/ArticleList.tsx @@ -1,48 +1,14 @@ 'use client'; import { getArticlesAPI } from '@/services/articles'; import ArticlePreview from './ArticlePreview'; -import { useInfiniteQuery } from '@tanstack/react-query'; -import React, { useEffect, useRef } from 'react'; -import { flexCenter, greenButton } from '@/styles/common.css'; -import useIntersectionObserver from '@/hooks/useIntersectionObserver'; +import React, { useRef } from 'react'; +import { flexCenter } from '@/styles/common.css'; +import useInfiniteScroll from '@/hooks/useInfiniteScroll'; const ArticleList = () => { const targetRef = useRef(null); - const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status } = useInfiniteQuery({ - queryKey: ['articles'], - queryFn: ({ pageParam = 1 }) => { - console.log(pageParam); - - return getArticlesAPI(pageParam); - }, - getNextPageParam: (lastPage, pages) => { - console.log('getNextPageParam 발동'); - - const totalPage = Math.ceil(lastPage.articlesCount / 10); - let currentPage = pages.length; - if (totalPage < pages.length) { - console.log('그만'); - - return undefined; - } - // console.log(totalPage); - // console.log(currentPage); - - return currentPage++; - }, - retry: false, - refetchOnWindowFocus: false, - }); - - const nextPage = () => { - if (!hasNextPage || isFetchingNextPage) { - return; - } - fetchNextPage(); - }; - - useIntersectionObserver(nextPage, targetRef); + const { data } = useInfiniteScroll(getArticlesAPI, targetRef); return (
diff --git a/hooks/useInfiniteScroll.ts b/hooks/useInfiniteScroll.ts new file mode 100644 index 00000000..f712b73e --- /dev/null +++ b/hooks/useInfiniteScroll.ts @@ -0,0 +1,44 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import useIntersectionObserver from './useIntersectionObserver'; +import { RefObject } from 'react'; + +const useInfiniteScroll = (fetcher: (page: any) => any, ref: RefObject) => { + const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status } = useInfiniteQuery({ + queryKey: ['articles'], + queryFn: ({ pageParam = 1 }) => { + console.log(pageParam); + + return fetcher(pageParam); + }, + getNextPageParam: (lastPage, pages) => { + console.log('getNextPageParam 발동'); + + const totalPage = Math.ceil(lastPage.articlesCount / 10); + let currentPage = pages.length; + if (totalPage < pages.length) { + console.log('그만'); + + return undefined; + } + // console.log(totalPage); + // console.log(currentPage); + + return currentPage++; + }, + retry: false, + refetchOnWindowFocus: false, + }); + + const nextPage = () => { + if (!hasNextPage || isFetchingNextPage) { + return; + } + fetchNextPage(); + }; + + useIntersectionObserver(nextPage, ref); + + return { data }; +}; + +export default useInfiniteScroll; From 50f225ef06cb96340609a23bb1e96b8d32448bec Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Mon, 18 Sep 2023 21:11:25 +0900 Subject: [PATCH 11/49] =?UTF-8?q?refactor:=20signup=20,=20login=20,=20logo?= =?UTF-8?q?ut=20=EA=B8=B0=EB=8A=A5=20route=20handler=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20=EB=B0=8F=20middleware=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=B2=B4=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/auth/login/route.ts | 24 ++++++++++++++++++++++++ app/api/auth/logout/route.ts | 11 +++++++++++ app/api/auth/route.ts | 20 -------------------- app/api/auth/signup/route.ts | 17 +++++++++++++++++ app/login/page.tsx | 7 +++---- app/settings/page.tsx | 14 +++++++++++++- components/layouts/SideBar.tsx | 3 +++ components/tags/TagList.tsx | 30 +++++++++++++++++++++++++----- hooks/useInfiniteScroll.ts | 4 +--- middleware.ts | 22 +++++++++++----------- 10 files changed, 108 insertions(+), 44 deletions(-) create mode 100644 app/api/auth/login/route.ts create mode 100644 app/api/auth/logout/route.ts delete mode 100644 app/api/auth/route.ts create mode 100644 app/api/auth/signup/route.ts diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts new file mode 100644 index 00000000..27d874bd --- /dev/null +++ b/app/api/auth/login/route.ts @@ -0,0 +1,24 @@ +import { http } from '@/utils/http'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + + const req = await http.post('/users/login', { user: body }); + + const response = NextResponse.json({ + message: 'Login successfull', + success: true, + }); + + response.cookies.set('token', req.user.token, { + httpOnly: true, + path: '/', + }); + + return response; + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 00000000..d569b320 --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + try { + const response = NextResponse.json({ message: 'Logout successful', success: true }); + response.cookies.set('token', '', { httpOnly: true, expires: new Date(0) }); + return response; + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/app/api/auth/route.ts b/app/api/auth/route.ts deleted file mode 100644 index 517a5b5b..00000000 --- a/app/api/auth/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { http } from '@/libs/http'; - -async function POST(req: NextRequest) { - const body = await req.json(); - - const res = await http.post(`${process.env.API_URL}/users/login`, body); - - console.log(res); - - const response = NextResponse.redirect('http://localhost:3000/', { status: 302 }); - response.cookies.set('auth', res?.user?.token, { - httpOnly: true, - secure: true, - }); - response.json(res); - return response; -} - -export { POST }; diff --git a/app/api/auth/signup/route.ts b/app/api/auth/signup/route.ts new file mode 100644 index 00000000..643ce8c3 --- /dev/null +++ b/app/api/auth/signup/route.ts @@ -0,0 +1,17 @@ +import { http } from '@/utils/http'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + console.log(body); + + const req = await http.post('/users', { user: body }); + + console.log(req); + + return NextResponse.json({ message: 'User created successfully', success: true, req }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/app/login/page.tsx b/app/login/page.tsx index 718cda6c..88b91943 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -20,7 +20,7 @@ const LoginPage = () => { }); const { mutate, isLoading } = useMutation({ - mutationFn: loginAPI, + mutationFn: (formData: any) => fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ ...formData }) }), onError: error => { setFormData({ email: '', @@ -29,9 +29,8 @@ const LoginPage = () => { alert('아이디 또는 비밀번호가 잘못되었습니다.'); }, onSuccess: res => { - login({ - ...res.user, - }); + console.log(res); + router.push('/'); }, }); diff --git a/app/settings/page.tsx b/app/settings/page.tsx index c583af52..2d299aa0 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -6,10 +6,13 @@ import { container, flex, hr, input } from '@/styles/common.css'; import { logoutButton, settingBlock, settingForm, settingTitle, updateButton } from '@/styles/settings.css'; import { User, UserAction } from '@/types'; import { useMutation } from '@tanstack/react-query'; +import { useRouter } from 'next/navigation'; + import { useState } from 'react'; const SettingsPage = () => { - const { email, username, image, bio, password, logout, updateUser } = useUserStore(); + const router = useRouter(); + const { email, username, image, bio, password, updateUser } = useUserStore(); // 초기화 함수로 전환 const [formData, setFormData] = useState({ @@ -48,6 +51,15 @@ const SettingsPage = () => { [e.target.name]: e.target.value, })); }; + + const logout = async () => { + try { + await fetch('/api/auth/logout'); + router.push('/login'); + } catch (error: any) { + console.error(error.message); + } + }; return (
diff --git a/components/layouts/SideBar.tsx b/components/layouts/SideBar.tsx index c0e48a54..5c37bed2 100644 --- a/components/layouts/SideBar.tsx +++ b/components/layouts/SideBar.tsx @@ -3,9 +3,12 @@ import TagList from '../tags/TagList'; import { sideBar, sideBarText } from '@/styles/layout.css'; import { sidePadding } from '@/styles/common.css'; import { http } from '@/utils/http'; +import { useQuery } from '@tanstack/react-query'; +import { getArticlesWithTagAPI } from '@/services/articles'; const SideBar = async () => { const { tags } = await http.get('/tags'); + return (
diff --git a/components/tags/TagList.tsx b/components/tags/TagList.tsx index bb664cba..c92be466 100644 --- a/components/tags/TagList.tsx +++ b/components/tags/TagList.tsx @@ -1,18 +1,38 @@ 'use client'; + +import getQueryClient from '@/libs/getQueryClient'; +import { getArticlesWithTagAPI } from '@/services/articles'; import { tagFill, tagItem, tagList } from '@/styles/layout.css'; +import { useMutation } from '@tanstack/react-query'; type Props = { tags: string[]; - onClick?: (tag: string) => void; }; -const TagList = ({ tags, onClick }: Props) => { +const TagList = ({ tags }: Props) => { + // const queryClient = getQueryClient(); + // const { data, mutate } = useMutation({ + // mutationKey: ['tag'], + // mutationFn: (tag: string) => getArticlesWithTagAPI(tag), + // onSuccess: res => { + // console.log(res); + // queryClient.invalidateQueries(['tag']); + // }, + // }); + + // const handleTagClick = (tag: string) => { + // mutate(tag); + // }; return (
    - {tags?.map((tag, index) => ( -
  • onClick(tag)}> + {/* {tags?.map((tag, index) => ( +
  • (handleTagClick ? handleTagClick(tag) : console.log('없음'))} + > {tag}
  • - ))} + ))} */}
); }; diff --git a/hooks/useInfiniteScroll.ts b/hooks/useInfiniteScroll.ts index f712b73e..b9abbb6c 100644 --- a/hooks/useInfiniteScroll.ts +++ b/hooks/useInfiniteScroll.ts @@ -3,7 +3,7 @@ import useIntersectionObserver from './useIntersectionObserver'; import { RefObject } from 'react'; const useInfiniteScroll = (fetcher: (page: any) => any, ref: RefObject) => { - const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status } = useInfiniteQuery({ + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ queryKey: ['articles'], queryFn: ({ pageParam = 1 }) => { console.log(pageParam); @@ -20,8 +20,6 @@ const useInfiniteScroll = (fetcher: (page: any) => any, ref: RefObject Date: Tue, 19 Sep 2023 00:43:58 +0900 Subject: [PATCH 12/49] =?UTF-8?q?feat=20&=20refactor:=20route=20handler?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20user=20api=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/articles/route.ts | 15 --------------- app/api/auth/login/route.ts | 4 ++-- app/api/auth/signup/route.ts | 11 +++-------- app/api/auth/user/route.ts | 24 ++++++++++++++++++++++++ app/api/comments/route.ts | 0 app/login/page.tsx | 4 ++-- app/settings/page.tsx | 15 +++------------ components/layouts/Header.tsx | 1 - components/tags/TagList.tsx | 7 ++++--- middleware.ts | 6 +++++- package-lock.json | 11 +++++++++++ package.json | 1 + services/users.ts | 3 --- stores/useUserStore.ts | 6 ++---- utils/cookies.ts | 33 ++++++++++----------------------- 15 files changed, 67 insertions(+), 74 deletions(-) create mode 100644 app/api/auth/user/route.ts create mode 100644 app/api/comments/route.ts diff --git a/app/api/articles/route.ts b/app/api/articles/route.ts index f19aa320..e69de29b 100644 --- a/app/api/articles/route.ts +++ b/app/api/articles/route.ts @@ -1,15 +0,0 @@ -import { fetchArticlesWithTag, registerArticle } from '@/services/articles'; -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(req: NextRequest) { - const tag = req.nextUrl.searchParams.get('tag') ?? ''; - const res = await fetchArticlesWithTag(tag); - return NextResponse.json(res); -} - -export async function POST(req: NextRequest) { - console.log(req.body); - - const res = await registerArticle(req.body); - return NextResponse.json(res); -} diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 27d874bd..318cc9e7 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -1,11 +1,11 @@ -import { http } from '@/utils/http'; +import { loginAPI } from '@/services/users'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { try { const body = await request.json(); - const req = await http.post('/users/login', { user: body }); + const req = await loginAPI(body); const response = NextResponse.json({ message: 'Login successfull', diff --git a/app/api/auth/signup/route.ts b/app/api/auth/signup/route.ts index 643ce8c3..4b665b96 100644 --- a/app/api/auth/signup/route.ts +++ b/app/api/auth/signup/route.ts @@ -1,16 +1,11 @@ -import { http } from '@/utils/http'; +import { registerUserAPI } from '@/services/users'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { try { const body = await request.json(); - console.log(body); - - const req = await http.post('/users', { user: body }); - - console.log(req); - - return NextResponse.json({ message: 'User created successfully', success: true, req }); + const res = await registerUserAPI(body); + return NextResponse.json({ message: 'User created successfully', success: true, res }); } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 500 }); } diff --git a/app/api/auth/user/route.ts b/app/api/auth/user/route.ts new file mode 100644 index 00000000..3b0e3e79 --- /dev/null +++ b/app/api/auth/user/route.ts @@ -0,0 +1,24 @@ +import { getUserAPI, updateUserAPI } from '@/services/users'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(request: NextRequest) { + try { + const token = request.cookies.get('token')?.value || ''; + const res = await getUserAPI(token); + return NextResponse.json({ message: 'Success', success: true, data: res }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } +} + +export async function PUT(request: NextRequest) { + try { + const body = await request.json(); + const token = request.cookies.get('token')?.value || ''; + const res = await updateUserAPI(body, token); + + return NextResponse.json({ message: 'Success', success: true, data: res }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } +} diff --git a/app/api/comments/route.ts b/app/api/comments/route.ts new file mode 100644 index 00000000..e69de29b diff --git a/app/login/page.tsx b/app/login/page.tsx index 88b91943..84e434fa 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,6 +1,5 @@ 'use client'; -import { loginAPI } from '@/services/users'; import useUserStore from '@/stores/useUserStore'; import { form, question, title } from '@/styles/account.css'; import { input, container, flexCenter, flexRow, fillGreenButton } from '@/styles/common.css'; @@ -20,7 +19,8 @@ const LoginPage = () => { }); const { mutate, isLoading } = useMutation({ - mutationFn: (formData: any) => fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ ...formData }) }), + mutationFn: (formData: any) => + fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ ...formData }) }).then(res => res.json()), onError: error => { setFormData({ email: '', diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 2d299aa0..0795d6aa 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,10 +1,9 @@ 'use client'; -import { updateUserAPI } from '@/services/users'; + import useUserStore from '@/stores/useUserStore'; import { articleTextarea } from '@/styles/article.css'; import { container, flex, hr, input } from '@/styles/common.css'; import { logoutButton, settingBlock, settingForm, settingTitle, updateButton } from '@/styles/settings.css'; -import { User, UserAction } from '@/types'; import { useMutation } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; @@ -23,17 +22,10 @@ const SettingsPage = () => { password, }); const { mutate, isLoading } = useMutation({ - mutationFn: updateUserAPI, - onError: err => { - console.error(err); - }, + mutationFn: (formData: any) => + fetch('/api/auth/user', { method: 'PUT', body: JSON.stringify(formData) }).then(res => res.json()), onSuccess: res => { - alert('회원 정보를 변경했습니다!'); console.log(res); - - updateUser({ - ...res.user, - }); }, }); @@ -115,7 +107,6 @@ const SettingsPage = () => {
- diff --git a/components/layouts/Header.tsx b/components/layouts/Header.tsx index 6330ec81..7644beb8 100644 --- a/components/layouts/Header.tsx +++ b/components/layouts/Header.tsx @@ -1,4 +1,3 @@ -'use client'; import { header, logo } from '@/styles/layout.css'; import Link from 'next/link'; import React from 'react'; diff --git a/components/tags/TagList.tsx b/components/tags/TagList.tsx index c92be466..d5d39923 100644 --- a/components/tags/TagList.tsx +++ b/components/tags/TagList.tsx @@ -22,17 +22,18 @@ const TagList = ({ tags }: Props) => { // const handleTagClick = (tag: string) => { // mutate(tag); // }; + return (
    - {/* {tags?.map((tag, index) => ( + {tags?.map((tag, index) => (
  • (handleTagClick ? handleTagClick(tag) : console.log('없음'))} + // onClick={() => (handleTagClick ? handleTagClick(tag) : console.log('없음'))} > {tag}
  • - ))} */} + ))}
); }; diff --git a/middleware.ts b/middleware.ts index 409f7def..65574885 100644 --- a/middleware.ts +++ b/middleware.ts @@ -6,6 +6,10 @@ export async function middleware(request: NextRequest) { const token = request.cookies.get('token')?.value || ''; + // if (path === '/api/user'){ + + // } + if (isPublic && token) { return NextResponse.redirect(new URL('/', request.nextUrl)); } @@ -16,5 +20,5 @@ export async function middleware(request: NextRequest) { } export const config = { - matcher: ['/settings', '/editor', '/login', '/register'], + matcher: ['/settings', '/editor', '/login', '/register', '/api/user'], }; diff --git a/package-lock.json b/package-lock.json index 0830a7e3..55188673 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@tanstack/react-query": "^4.33.0", "@vanilla-extract/css": "^1.13.0", "axios": "^1.5.0", + "jwt-decode": "^3.1.2", "next": "^13.4.19", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -4698,6 +4699,11 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -10506,6 +10512,11 @@ "object.values": "^1.1.6" } }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", diff --git a/package.json b/package.json index 6ecc4e70..a9f79fb3 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@tanstack/react-query": "^4.33.0", "@vanilla-extract/css": "^1.13.0", "axios": "^1.5.0", + "jwt-decode": "^3.1.2", "next": "^13.4.19", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/services/users.ts b/services/users.ts index 8226a438..c0e8c6b2 100644 --- a/services/users.ts +++ b/services/users.ts @@ -26,9 +26,6 @@ const updateUserAPI = async (user: UpdateUser, auth: string) => { // 현재 유저 조회 const getUserAPI = async (auth: string) => { - console.log('getUserAPI'); - console.log(auth); - return http.get('/user', { headers: { 'Content-Type': 'application/json; charset=utf-8', diff --git a/stores/useUserStore.ts b/stores/useUserStore.ts index 4d682ed1..05843d91 100644 --- a/stores/useUserStore.ts +++ b/stores/useUserStore.ts @@ -1,5 +1,4 @@ import { User, UserAction } from '@/types'; -import { setCookie } from '@/utils/cookies'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; @@ -18,7 +17,7 @@ const useUserStore = create( login: user => { set(() => { const { email, username, bio, image, token } = user; - setCookie('token', token, 60 * 60 * 24); + return { email, username, @@ -29,13 +28,12 @@ const useUserStore = create( }, logout: () => { set(() => { - setCookie('token', '', 0); return { ...initialState, }; }); }, - updateUser: user => { + updateUser: (user: User) => { set(() => { return { ...user, diff --git a/utils/cookies.ts b/utils/cookies.ts index 9d8d5e63..8aa05a95 100644 --- a/utils/cookies.ts +++ b/utils/cookies.ts @@ -1,25 +1,12 @@ -'use client'; - -export const setCookie = (key: string, value: string, maxAge: number) => { - document.cookie = `${key}=${value};max-age=${maxAge}`; -}; - -export const getCookie = (key: string) => { - const cookie = document.cookie.split('; ').find(v => v.split('=')[0] === key); - - if (!cookie) { - return; +import jwtDecode from 'jwt-decode'; +import { NextRequest } from 'next/server'; + +export const getDataFromToken = (request: NextRequest) => { + try { + const token = request.cookies.get('token')?.value || ''; + const decodedToken = jwtDecode(token); + return decodedToken; + } catch (error: any) { + throw new Error(error.message); } - - const [, value] = cookie.split('='); - - return value; -}; - -export const deleteAllCookies = () => { - const cookies = document.cookie.split(';'); - - cookies.forEach((_, idx) => { - document.cookie = cookies[idx] + '=;expires=' + new Date(0).toUTCString(); - }); }; From 32bceb4eb0fa56ba4de2297e7e8229a9f15b5412 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Tue, 26 Sep 2023 19:06:42 +0900 Subject: [PATCH 13/49] =?UTF-8?q?feat:=20react=20query=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/user/route.ts | 10 ++++++++-- app/login/page.tsx | 12 ++++++++---- app/settings/page.tsx | 20 +++++++++++++++----- components/layouts/NavBar.tsx | 13 ++++++++++--- components/tags/TagList.tsx | 17 ----------------- 5 files changed, 41 insertions(+), 31 deletions(-) diff --git a/app/api/user/route.ts b/app/api/user/route.ts index f6185102..db999ac0 100644 --- a/app/api/user/route.ts +++ b/app/api/user/route.ts @@ -1,12 +1,18 @@ -import { NextRequest } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; import { getUserAPI, updateUserAPI } from '@/services/users'; export async function GET(req: NextRequest) { const { value } = req.cookies.get('token'); console.log(value); console.log('route user get'); + const { user } = await getUserAPI(value); + console.log(user); - return getUserAPI(value); + return NextResponse.json({ + message: 'Login successfull', + success: true, + user, + }); } export async function PUT(req: NextRequest) { diff --git a/app/login/page.tsx b/app/login/page.tsx index 84e434fa..c065ba4e 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,23 +1,27 @@ 'use client'; -import useUserStore from '@/stores/useUserStore'; import { form, question, title } from '@/styles/account.css'; import { input, container, flexCenter, flexRow, fillGreenButton } from '@/styles/common.css'; import { buttonBox } from '@/styles/layout.css'; import { LoginUser } from '@/types'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import { ChangeEvent, FormEvent, useState } from 'react'; const LoginPage = () => { const router = useRouter(); - const { login } = useUserStore(); const [formData, setFormData] = useState({ email: '', password: '', }); + const { data: userData, refetch } = useQuery({ + queryKey: ['user-data'], + queryFn: () => fetch('/api/user').then(res => res.json()), + enabled: false, + }); + const { mutate, isLoading } = useMutation({ mutationFn: (formData: any) => fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ ...formData }) }).then(res => res.json()), @@ -30,7 +34,7 @@ const LoginPage = () => { }, onSuccess: res => { console.log(res); - + refetch(); router.push('/'); }, }); diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 0795d6aa..5d51a482 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,17 +1,26 @@ 'use client'; - -import useUserStore from '@/stores/useUserStore'; +import getQueryClient from '@/libs/getQueryClient'; import { articleTextarea } from '@/styles/article.css'; import { container, flex, hr, input } from '@/styles/common.css'; import { logoutButton, settingBlock, settingForm, settingTitle, updateButton } from '@/styles/settings.css'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; const SettingsPage = () => { const router = useRouter(); - const { email, username, image, bio, password, updateUser } = useUserStore(); + + const { data: userData, refetch } = useQuery({ + queryKey: ['user-data'], + queryFn: () => fetch('/api/user').then(res => res.json()), + }); + + const queryClient = getQueryClient(); + + const { + user: { email, username, image, bio }, + } = userData; // 초기화 함수로 전환 const [formData, setFormData] = useState({ @@ -19,7 +28,7 @@ const SettingsPage = () => { username, bio: bio ? bio : '', email, - password, + password: '', }); const { mutate, isLoading } = useMutation({ mutationFn: (formData: any) => @@ -47,6 +56,7 @@ const SettingsPage = () => { const logout = async () => { try { await fetch('/api/auth/logout'); + router.push('/login'); } catch (error: any) { console.error(error.message); diff --git a/components/layouts/NavBar.tsx b/components/layouts/NavBar.tsx index 66c06af6..b1b3d2ce 100644 --- a/components/layouts/NavBar.tsx +++ b/components/layouts/NavBar.tsx @@ -1,12 +1,11 @@ 'use client'; import * as styles from '@/styles/layout.css'; -import useUserStore from '@/stores/useUserStore'; import { usePathname } from 'next/navigation'; - import Link from 'next/link'; import { userImageSm } from '@/styles/profile.css'; import Image from 'next/image'; import { EditIcon, SettingIcon } from '@/composables/icons'; +import { useQuery } from '@tanstack/react-query'; const NAVS = [ { @@ -36,7 +35,15 @@ const NAVS = [ ]; const NavBar = () => { - const { username, image } = useUserStore(); + const { data: userData } = useQuery({ + queryKey: ['user-data'], + queryFn: () => fetch('/api/user').then(res => res.json()), + }); + + const { + user: { username, image }, + } = userData; + const pathname = usePathname(); return ( diff --git a/components/tags/TagList.tsx b/components/tags/TagList.tsx index d5d39923..fd596ffc 100644 --- a/components/tags/TagList.tsx +++ b/components/tags/TagList.tsx @@ -1,28 +1,11 @@ 'use client'; -import getQueryClient from '@/libs/getQueryClient'; -import { getArticlesWithTagAPI } from '@/services/articles'; import { tagFill, tagItem, tagList } from '@/styles/layout.css'; -import { useMutation } from '@tanstack/react-query'; type Props = { tags: string[]; }; const TagList = ({ tags }: Props) => { - // const queryClient = getQueryClient(); - // const { data, mutate } = useMutation({ - // mutationKey: ['tag'], - // mutationFn: (tag: string) => getArticlesWithTagAPI(tag), - // onSuccess: res => { - // console.log(res); - // queryClient.invalidateQueries(['tag']); - // }, - // }); - - // const handleTagClick = (tag: string) => { - // mutate(tag); - // }; - return (
    {tags?.map((tag, index) => ( From 1dc77fba64ec86a0a2f2c8eae93aacc83523a935 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Wed, 27 Sep 2023 03:52:16 +0900 Subject: [PATCH 14/49] =?UTF-8?q?feat:=20tag=EB=A1=9C=20article=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/auth/user/route.ts | 1 + app/settings/page.tsx | 27 +++++++------- components/article/ArticleList.tsx | 7 ++++ components/article/ArticleTab.tsx | 6 ++- components/layouts/NavBar.tsx | 14 +++---- components/layouts/SideBar.tsx | 3 -- components/tags/Tag.tsx | 16 ++++++++ components/tags/TagList.tsx | 24 ++++++------ hooks/useArticles.ts | 10 +++++ hooks/useInfiniteScroll.ts | 8 ---- libs/Providers.tsx | 2 + libs/getQueryClient.ts | 11 +----- stores/useCurrentTag.ts | 8 ++++ utils/http.ts | 60 ++---------------------------- 14 files changed, 85 insertions(+), 112 deletions(-) create mode 100644 components/tags/Tag.tsx create mode 100644 hooks/useArticles.ts create mode 100644 stores/useCurrentTag.ts diff --git a/app/api/auth/user/route.ts b/app/api/auth/user/route.ts index 3b0e3e79..88d3fe4b 100644 --- a/app/api/auth/user/route.ts +++ b/app/api/auth/user/route.ts @@ -16,6 +16,7 @@ export async function PUT(request: NextRequest) { const body = await request.json(); const token = request.cookies.get('token')?.value || ''; const res = await updateUserAPI(body, token); + console.log(res); return NextResponse.json({ message: 'Success', success: true, data: res }); } catch (error: any) { diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 5d51a482..089cd6f6 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,26 +1,19 @@ 'use client'; -import getQueryClient from '@/libs/getQueryClient'; import { articleTextarea } from '@/styles/article.css'; import { container, flex, hr, input } from '@/styles/common.css'; import { logoutButton, settingBlock, settingForm, settingTitle, updateButton } from '@/styles/settings.css'; -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; const SettingsPage = () => { const router = useRouter(); - - const { data: userData, refetch } = useQuery({ - queryKey: ['user-data'], - queryFn: () => fetch('/api/user').then(res => res.json()), - }); - - const queryClient = getQueryClient(); + const queryClient = useQueryClient(); const { user: { email, username, image, bio }, - } = userData; + } = queryClient.getQueryData(['user-data']); // 초기화 함수로 전환 const [formData, setFormData] = useState({ @@ -33,8 +26,16 @@ const SettingsPage = () => { const { mutate, isLoading } = useMutation({ mutationFn: (formData: any) => fetch('/api/auth/user', { method: 'PUT', body: JSON.stringify(formData) }).then(res => res.json()), - onSuccess: res => { - console.log(res); + onSuccess: data => { + console.log(data); + console.log('성공'); + + queryClient.setQueryData(['user-data'], (oldQueryData: any) => { + return { + ...oldQueryData, + data: [...oldQueryData.user, data.user], + }; + }); }, }); @@ -56,7 +57,7 @@ const SettingsPage = () => { const logout = async () => { try { await fetch('/api/auth/logout'); - + queryClient.removeQueries(['user-data']); router.push('/login'); } catch (error: any) { console.error(error.message); diff --git a/components/article/ArticleList.tsx b/components/article/ArticleList.tsx index 99cf2be1..4ca18bd6 100644 --- a/components/article/ArticleList.tsx +++ b/components/article/ArticleList.tsx @@ -4,12 +4,19 @@ import ArticlePreview from './ArticlePreview'; import React, { useRef } from 'react'; import { flexCenter } from '@/styles/common.css'; import useInfiniteScroll from '@/hooks/useInfiniteScroll'; +import { useQueryClient } from '@tanstack/react-query'; +import useCurrentTag from '@/stores/useCurrentTag'; const ArticleList = () => { const targetRef = useRef(null); + const { tag } = useCurrentTag(); const { data } = useInfiniteScroll(getArticlesAPI, targetRef); + const queryClient = useQueryClient(); + const query = queryClient.getQueryData(['articles-tag', tag]); + console.log(query); + return (
    {data?.pages.map((group, i) => ( diff --git a/components/article/ArticleTab.tsx b/components/article/ArticleTab.tsx index 9a741691..e2948c9f 100644 --- a/components/article/ArticleTab.tsx +++ b/components/article/ArticleTab.tsx @@ -1,12 +1,16 @@ +'use client'; +import useCurrentTag from '@/stores/useCurrentTag'; import { articleTab, articleTabItem, articleTabItemActivate, articleTabItemDisable } from '@/styles/article.css'; const ArticleTab = () => { const user = false; + const { tag } = useCurrentTag(); + return (
      {user &&
    • Your Feed
    • }
    • Global Feed
    • - {user &&
    • # {}
    • } + {tag &&
    • # {tag}
    • }
    ); }; diff --git a/components/layouts/NavBar.tsx b/components/layouts/NavBar.tsx index b1b3d2ce..8459105f 100644 --- a/components/layouts/NavBar.tsx +++ b/components/layouts/NavBar.tsx @@ -5,7 +5,7 @@ import Link from 'next/link'; import { userImageSm } from '@/styles/profile.css'; import Image from 'next/image'; import { EditIcon, SettingIcon } from '@/composables/icons'; -import { useQuery } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; const NAVS = [ { @@ -35,14 +35,12 @@ const NAVS = [ ]; const NavBar = () => { - const { data: userData } = useQuery({ - queryKey: ['user-data'], - queryFn: () => fetch('/api/user').then(res => res.json()), - }); + const queryClient = useQueryClient(); - const { - user: { username, image }, - } = userData; + const userQuery = queryClient.getQueryData(['user-data']); + + const username = userQuery?.user?.username; + const image = userQuery?.user?.image; const pathname = usePathname(); diff --git a/components/layouts/SideBar.tsx b/components/layouts/SideBar.tsx index 5c37bed2..b0207c72 100644 --- a/components/layouts/SideBar.tsx +++ b/components/layouts/SideBar.tsx @@ -3,9 +3,6 @@ import TagList from '../tags/TagList'; import { sideBar, sideBarText } from '@/styles/layout.css'; import { sidePadding } from '@/styles/common.css'; import { http } from '@/utils/http'; -import { useQuery } from '@tanstack/react-query'; -import { getArticlesWithTagAPI } from '@/services/articles'; - const SideBar = async () => { const { tags } = await http.get('/tags'); diff --git a/components/tags/Tag.tsx b/components/tags/Tag.tsx new file mode 100644 index 00000000..1525ec63 --- /dev/null +++ b/components/tags/Tag.tsx @@ -0,0 +1,16 @@ +import { tagFill, tagItem } from '@/styles/layout.css'; + +type Props = { + tag: string; + onTagClick: (tag: string) => void; +}; + +const Tag = ({ tag, onTagClick }: Props) => { + return ( +
  • onTagClick(tag)}> + {tag} +
  • + ); +}; + +export default Tag; diff --git a/components/tags/TagList.tsx b/components/tags/TagList.tsx index fd596ffc..212e9eb0 100644 --- a/components/tags/TagList.tsx +++ b/components/tags/TagList.tsx @@ -1,23 +1,21 @@ 'use client'; - -import { tagFill, tagItem, tagList } from '@/styles/layout.css'; +import { tagList } from '@/styles/layout.css'; +import Tag from './Tag'; +import { useArticlesByTag } from '@/hooks/useArticles'; +import useCurrentTag from '@/stores/useCurrentTag'; type Props = { tags: string[]; }; const TagList = ({ tags }: Props) => { + const { tag, setTag } = useCurrentTag(); + const { data } = useArticlesByTag(tag); + + const handleTagClick = (tag: string) => { + setTag(tag); + }; return ( -
      - {tags?.map((tag, index) => ( -
    • (handleTagClick ? handleTagClick(tag) : console.log('없음'))} - > - {tag} -
    • - ))} -
    +
      {tags?.map((tag, index) => )}
    ); }; diff --git a/hooks/useArticles.ts b/hooks/useArticles.ts new file mode 100644 index 00000000..cb4d3a38 --- /dev/null +++ b/hooks/useArticles.ts @@ -0,0 +1,10 @@ +import { getArticlesWithTagAPI } from '@/services/articles'; +import { useQuery } from '@tanstack/react-query'; + +export const useArticlesByTag = (tag: string) => { + return useQuery({ + queryKey: ['articles-tag', tag], + queryFn: () => getArticlesWithTagAPI(tag), + enabled: !!tag, + }); +}; diff --git a/hooks/useInfiniteScroll.ts b/hooks/useInfiniteScroll.ts index b9abbb6c..242e2ef4 100644 --- a/hooks/useInfiniteScroll.ts +++ b/hooks/useInfiniteScroll.ts @@ -6,25 +6,17 @@ const useInfiniteScroll = (fetcher: (page: any) => any, ref: RefObject { - console.log(pageParam); - return fetcher(pageParam); }, getNextPageParam: (lastPage, pages) => { - console.log('getNextPageParam 발동'); - const totalPage = Math.ceil(lastPage.articlesCount / 10); let currentPage = pages.length; if (totalPage < pages.length) { - console.log('그만'); - return undefined; } return currentPage++; }, - retry: false, - refetchOnWindowFocus: false, }); const nextPage = () => { diff --git a/libs/Providers.tsx b/libs/Providers.tsx index 389bace9..2d7f1950 100644 --- a/libs/Providers.tsx +++ b/libs/Providers.tsx @@ -13,6 +13,8 @@ export default function Providers({ children }: PropsWithChildren) { defaultOptions: { queries: { suspense: true, + refetchOnWindowFocus: false, + retry: false, }, }, }) diff --git a/libs/getQueryClient.ts b/libs/getQueryClient.ts index 9a67a9a6..b78ce01e 100644 --- a/libs/getQueryClient.ts +++ b/libs/getQueryClient.ts @@ -1,14 +1,5 @@ import { QueryClient } from '@tanstack/query-core'; import { cache } from 'react'; -const getQueryClient = cache( - () => - new QueryClient({ - defaultOptions: { - queries: { - suspense: true, - }, - }, - }) -); +const getQueryClient = cache(() => new QueryClient()); export default getQueryClient; diff --git a/stores/useCurrentTag.ts b/stores/useCurrentTag.ts new file mode 100644 index 00000000..2e2f81ac --- /dev/null +++ b/stores/useCurrentTag.ts @@ -0,0 +1,8 @@ +import { create } from 'zustand'; + +const useCurrentTag = create(set => ({ + tag: '', + setTag: (tag: string) => set(() => ({ tag })), +})); + +export default useCurrentTag; diff --git a/utils/http.ts b/utils/http.ts index 66ad06b3..4837e245 100644 --- a/utils/http.ts +++ b/utils/http.ts @@ -10,7 +10,7 @@ export const http = { ...options, }; // console.log('서버자나'); - console.log(API_BASE_URL); + // console.log(API_BASE_URL); // console.log(defaultOptions); @@ -21,12 +21,15 @@ export const http = { const errorData = await response.json(); // console.log('error'); // console.log(errorData); + console.log('실패'); throw new Error(errorData.message || 'Request failed'); } // console.log(response); + console.log('성공'); + return response.json(); } catch (error) { console.error(error); @@ -39,58 +42,3 @@ export const http = { put: (url: string, body?: any, options?: any) => http.request(url, 'PUT', body, options), delete: (url: string, options?: any) => http.request(url, 'DELETE', undefined, options), }; - -// export const http = { -// get: (url: string, options?: any) => { -// return fetch(`${API_BASE_URL}${url}`, { ...options, method: 'GET' }) -// .then(res => { -// if (!(res.status === 200 || res.status === 201)) { -// throw new Error('Error!'); -// } -// return res.json(); -// }) -// .catch(err => { -// console.error(err); -// throw new Error('Error!'); -// }); -// }, -// post: (url: string, body?: Request, options?: any) => { -// return fetch(`${API_BASE_URL}${url}`, { ...options, method: 'POST', body: JSON.stringify(body) }) -// .then(res => { -// if (!(res.status === 200 || res.status === 201)) { -// throw new Error('Error!'); -// } -// return res.json(); -// }) -// .catch(err => { -// console.error(err); -// throw new Error('Error!'); -// }); -// }, -// put: (url: string, body?: Request, options?: any) => { -// return fetch(`${API_BASE_URL}${url}`, { ...options, method: 'PUT', body: JSON.stringify(body) }) -// .then(res => { -// if (!(res.status === 200 || res.status === 201)) { -// throw new Error('Error!'); -// } -// return res.json(); -// }) -// .catch(err => { -// console.error(err); -// throw new Error('Error!'); -// }); -// }, -// delete: (url: string, options?: any) => { -// return fetch(`${API_BASE_URL}${url}`, { ...options, method: 'DELETE' }) -// .then(res => { -// if (!(res.status === 200 || res.status === 201)) { -// throw new Error('Error!'); -// } -// return res.json(); -// }) -// .catch(err => { -// console.error(err); -// throw new Error('Error!'); -// }); -// }, -// }; From 209d8ab57b067b49dc6b87530d3d02f5528f9001 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Wed, 27 Sep 2023 04:15:06 +0900 Subject: [PATCH 15/49] =?UTF-8?q?refactor:=20useUserStore=EB=A1=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/auth/login/route.ts | 1 + app/login/page.tsx | 4 +++- app/settings/page.tsx | 22 +++++++--------------- components/layouts/NavBar.tsx | 10 ++-------- stores/useUserStore.ts | 3 +-- types/index.ts | 2 +- 6 files changed, 15 insertions(+), 27 deletions(-) diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 318cc9e7..0c44f4fb 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -10,6 +10,7 @@ export async function POST(request: NextRequest) { const response = NextResponse.json({ message: 'Login successfull', success: true, + user: req.user, }); response.cookies.set('token', req.user.token, { diff --git a/app/login/page.tsx b/app/login/page.tsx index c065ba4e..92fdcc02 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import useUserStore from '@/stores/useUserStore'; import { form, question, title } from '@/styles/account.css'; import { input, container, flexCenter, flexRow, fillGreenButton } from '@/styles/common.css'; import { buttonBox } from '@/styles/layout.css'; @@ -10,6 +11,7 @@ import { ChangeEvent, FormEvent, useState } from 'react'; const LoginPage = () => { const router = useRouter(); + const { login } = useUserStore(); const [formData, setFormData] = useState({ email: '', @@ -34,7 +36,7 @@ const LoginPage = () => { }, onSuccess: res => { console.log(res); - refetch(); + login({ ...res.user }); router.push('/'); }, }); diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 089cd6f6..b8dc6a74 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,19 +1,17 @@ 'use client'; +import useUserStore from '@/stores/useUserStore'; import { articleTextarea } from '@/styles/article.css'; import { container, flex, hr, input } from '@/styles/common.css'; import { logoutButton, settingBlock, settingForm, settingTitle, updateButton } from '@/styles/settings.css'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; const SettingsPage = () => { const router = useRouter(); - const queryClient = useQueryClient(); - const { - user: { email, username, image, bio }, - } = queryClient.getQueryData(['user-data']); + const { logout, email, username, image, bio } = useUserStore(); // 초기화 함수로 전환 const [formData, setFormData] = useState({ @@ -29,13 +27,6 @@ const SettingsPage = () => { onSuccess: data => { console.log(data); console.log('성공'); - - queryClient.setQueryData(['user-data'], (oldQueryData: any) => { - return { - ...oldQueryData, - data: [...oldQueryData.user, data.user], - }; - }); }, }); @@ -54,10 +45,11 @@ const SettingsPage = () => { })); }; - const logout = async () => { + const signOut = async () => { try { await fetch('/api/auth/logout'); - queryClient.removeQueries(['user-data']); + + logout(); router.push('/login'); } catch (error: any) { console.error(error.message); @@ -118,7 +110,7 @@ const SettingsPage = () => {
    -
    diff --git a/components/layouts/NavBar.tsx b/components/layouts/NavBar.tsx index 8459105f..24a7661a 100644 --- a/components/layouts/NavBar.tsx +++ b/components/layouts/NavBar.tsx @@ -5,7 +5,7 @@ import Link from 'next/link'; import { userImageSm } from '@/styles/profile.css'; import Image from 'next/image'; import { EditIcon, SettingIcon } from '@/composables/icons'; -import { useQueryClient } from '@tanstack/react-query'; +import useUserStore from '@/stores/useUserStore'; const NAVS = [ { @@ -35,13 +35,7 @@ const NAVS = [ ]; const NavBar = () => { - const queryClient = useQueryClient(); - - const userQuery = queryClient.getQueryData(['user-data']); - - const username = userQuery?.user?.username; - const image = userQuery?.user?.image; - + const { username, image } = useUserStore(); const pathname = usePathname(); return ( diff --git a/stores/useUserStore.ts b/stores/useUserStore.ts index 05843d91..b4f367c6 100644 --- a/stores/useUserStore.ts +++ b/stores/useUserStore.ts @@ -5,7 +5,6 @@ import { persist } from 'zustand/middleware'; const initialState: User = { username: '', email: '', - token: '', bio: '', image: '', }; @@ -16,7 +15,7 @@ const useUserStore = create( ...initialState, login: user => { set(() => { - const { email, username, bio, image, token } = user; + const { email, username, bio, image } = user; return { email, diff --git a/types/index.ts b/types/index.ts index 47a64d9d..82b43efe 100644 --- a/types/index.ts +++ b/types/index.ts @@ -25,7 +25,7 @@ export type NewUser = { export type User = { email: string; - token: string; + token?: string; username: string; bio: string; image: string; From 04c106236e05e1116fcfa2cf7d0598d2b23bb7c4 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Fri, 29 Sep 2023 05:51:28 +0900 Subject: [PATCH 16/49] =?UTF-8?q?feat:=20useArticles=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=ED=9B=85=EC=9D=84=20=ED=86=B5=ED=95=B4=20Articles?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=8C=A8?= =?UTF-8?q?=EC=B9=AD=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/articles/favorite/[slug]/route.ts | 31 +++++++++++++++ app/api/articles/feed/route.ts | 17 ++++++++ app/api/articles/route.ts | 0 app/article/[slug]/page.tsx | 29 ++++---------- app/login/page.tsx | 8 +--- app/page.tsx | 4 +- components/article/ArticleList.tsx | 30 +++++++------- components/article/ArticlePreview.tsx | 13 ++++-- components/article/ArticleTab.tsx | 29 +++++++++++--- components/comments/CommentBox.tsx | 27 +++++++++++++ components/tags/TagList.tsx | 8 ++-- hooks/useArticles.ts | 48 +++++++++++++++++++---- middleware.ts | 8 ++-- services/articles.ts | 40 ++++++++++++++++--- stores/useCurrentTab.ts | 8 ++++ stores/useCurrentTag.ts | 8 ---- utils/http.ts | 2 + 17 files changed, 226 insertions(+), 84 deletions(-) create mode 100644 app/api/articles/favorite/[slug]/route.ts create mode 100644 app/api/articles/feed/route.ts delete mode 100644 app/api/articles/route.ts create mode 100644 components/comments/CommentBox.tsx create mode 100644 stores/useCurrentTab.ts delete mode 100644 stores/useCurrentTag.ts diff --git a/app/api/articles/favorite/[slug]/route.ts b/app/api/articles/favorite/[slug]/route.ts new file mode 100644 index 00000000..538d4571 --- /dev/null +++ b/app/api/articles/favorite/[slug]/route.ts @@ -0,0 +1,31 @@ +import { favoriteAPI, unFavoriteAPI } from '@/services/articles'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest, route: { params: { slug: string } }) { + try { + console.log('좋아요'); + + console.log(route.params.slug); + + const slug = route.params.slug; + const token = request.cookies.get('token')?.value || ''; + const res = await favoriteAPI(slug, token); + console.log(res); + + return NextResponse.json({ message: 'Favorite Success', success: true, data: res }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } +} + +export async function DELETE(request: NextRequest) { + try { + console.log('좋아요 취소'); + const slug = await request.json(); + const token = request.cookies.get('token')?.value || ''; + const res = await unFavoriteAPI(slug, token); + return NextResponse.json({ message: 'Un Favorite Success', success: true, data: res }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } +} diff --git a/app/api/articles/feed/route.ts b/app/api/articles/feed/route.ts new file mode 100644 index 00000000..719127ea --- /dev/null +++ b/app/api/articles/feed/route.ts @@ -0,0 +1,17 @@ +import { getArticlesFeed } from '@/services/articles'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const page = searchParams.get('page'); + + const token = request.cookies.get('token')?.value || ''; + console.log(token); + + const { articles, articlesCount } = await getArticlesFeed(Number(page), token); + console.log(articles); + console.log(articlesCount); + + return NextResponse.json({ articles, articlesCount }); +} diff --git a/app/api/articles/route.ts b/app/api/articles/route.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/app/article/[slug]/page.tsx b/app/article/[slug]/page.tsx index 327832fa..e378a511 100644 --- a/app/article/[slug]/page.tsx +++ b/app/article/[slug]/page.tsx @@ -1,22 +1,21 @@ -import CommentCard from '@/components/comments/CommentCard'; -import CommentForm from '@/components/comments/CommentForm'; +import CommentBox from '@/components/comments/CommentBox'; import Banner from '@/components/layouts/Banner'; import TagList from '@/components/tags/TagList'; import FavoriteButton from '@/components/user/FavoriteButton'; import FollowButton from '@/components/user/FollowButton'; import UserBox from '@/components/user/UserBox'; -import { fetchArticle } from '@/services/articles'; +import { getArticleAPI } from '@/services/articles'; import { articleContent, articleDetailTitle } from '@/styles/article.css'; -import { container, flex, flexCenter, flexRow, justifyCenter, paddingTB, textCenter } from '@/styles/common.css'; -import { Article } from '@/types'; -import Link from 'next/link'; +import { container, flex, justifyCenter, paddingTB } from '@/styles/common.css'; import React from 'react'; type Props = { params: { slug: string }; }; const ArticlePage = async ({ params: { slug } }: Props) => { - const { title, author, createdAt, body, tagList, favoritesCount } = await fetchArticle
    (slug); - const user = true; + const { + article: { title, author, createdAt, body, tagList, favoritesCount }, + } = await getArticleAPI(slug); + return (
    @@ -36,20 +35,8 @@ const ArticlePage = async ({ params: { slug } }: Props) => {  
    -
    - {user ? ( -
    - - -
    - ) : ( -
    - Sign in or sign up to add comments on this - article. -
    - )} -
+
); }; diff --git a/app/login/page.tsx b/app/login/page.tsx index 92fdcc02..61b9640e 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -5,7 +5,7 @@ import { form, question, title } from '@/styles/account.css'; import { input, container, flexCenter, flexRow, fillGreenButton } from '@/styles/common.css'; import { buttonBox } from '@/styles/layout.css'; import { LoginUser } from '@/types'; -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import { ChangeEvent, FormEvent, useState } from 'react'; @@ -18,12 +18,6 @@ const LoginPage = () => { password: '', }); - const { data: userData, refetch } = useQuery({ - queryKey: ['user-data'], - queryFn: () => fetch('/api/user').then(res => res.json()), - enabled: false, - }); - const { mutate, isLoading } = useMutation({ mutationFn: (formData: any) => fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ ...formData }) }).then(res => res.json()), diff --git a/app/page.tsx b/app/page.tsx index a792e799..a1b4e424 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,12 +1,14 @@ import ArticleList from '@/components/article/ArticleList'; -import ArticleTab from '@/components/article/ArticleTab'; import Banner from '@/components/layouts/Banner'; import SideBar from '@/components/layouts/SideBar'; import { articleContainer } from '@/styles/article.css'; import { container, flex } from '@/styles/common.css'; import { bannerDescription, bannerTitle } from '@/styles/home.css'; +import dynamic from 'next/dynamic'; import { Suspense } from 'react'; +const ArticleTab = dynamic(() => import('@/components/article/ArticleTab'), { ssr: false }); + export default function Page() { return (
diff --git a/components/article/ArticleList.tsx b/components/article/ArticleList.tsx index 4ca18bd6..15a07dfc 100644 --- a/components/article/ArticleList.tsx +++ b/components/article/ArticleList.tsx @@ -1,31 +1,29 @@ 'use client'; -import { getArticlesAPI } from '@/services/articles'; + import ArticlePreview from './ArticlePreview'; import React, { useRef } from 'react'; import { flexCenter } from '@/styles/common.css'; -import useInfiniteScroll from '@/hooks/useInfiniteScroll'; -import { useQueryClient } from '@tanstack/react-query'; -import useCurrentTag from '@/stores/useCurrentTag'; +import useCurrentTab from '@/stores/useCurrentTab'; +import useArticles from '@/hooks/useArticles'; const ArticleList = () => { const targetRef = useRef(null); - const { tag } = useCurrentTag(); - - const { data } = useInfiniteScroll(getArticlesAPI, targetRef); - - const queryClient = useQueryClient(); - const query = queryClient.getQueryData(['articles-tag', tag]); - console.log(query); + const { tab } = useCurrentTab(); + const { data } = useArticles(targetRef, tab); return (
- {data?.pages.map((group, i) => ( -
- {group.articles.map(article => ( - + {data?.pages?.at(-1)?.articles?.length === 0 ? ( + '데이터가 없습니다.' + ) : ( +
+ {data?.pages.map((group, i) => ( +
+ {group?.articles?.map(article => )} +
))}
- ))} + )}
); diff --git a/components/article/ArticlePreview.tsx b/components/article/ArticlePreview.tsx index a7a7a7f0..a89d2336 100644 --- a/components/article/ArticlePreview.tsx +++ b/components/article/ArticlePreview.tsx @@ -6,7 +6,6 @@ import { useRouter } from 'next/navigation'; import { FillHeartIcon } from '@/composables/icons'; import { fillGreenButton, flex, flexBetween, greenButton } from '@/styles/common.css'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { favoriteAPI, unFavoriteAPI } from '@/services/articles'; type Props = { article: any; @@ -19,11 +18,19 @@ const ArticlePreview = ({ const queryClient = useQueryClient(); const { mutate } = useMutation({ - mutationFn: favorited ? favoriteAPI : unFavoriteAPI, + mutationFn: async (slug: string) => { + const method = favorited ? 'DELETE' : 'POST'; + console.log(slug); + + return fetch(`/api/articles/favorite/${slug}`, { method }).then(res => res.json()); + }, onError: err => { - console.error(err); + // 권한이 없을 경우 login 페이지로 이동 + router.push('/login'); }, onSuccess: res => { + console.log('성공 후 리패치'); + queryClient.invalidateQueries({ queryKey: ['articles'] }); }, }); diff --git a/components/article/ArticleTab.tsx b/components/article/ArticleTab.tsx index e2948c9f..92167d50 100644 --- a/components/article/ArticleTab.tsx +++ b/components/article/ArticleTab.tsx @@ -1,16 +1,33 @@ 'use client'; -import useCurrentTag from '@/stores/useCurrentTag'; +import useCurrentTab from '@/stores/useCurrentTab'; +import useUserStore from '@/stores/useUserStore'; import { articleTab, articleTabItem, articleTabItemActivate, articleTabItemDisable } from '@/styles/article.css'; const ArticleTab = () => { - const user = false; - const { tag } = useCurrentTag(); + const { email } = useUserStore(); + const { tab, setTab } = useCurrentTab(); + + const handleTabClick = (tab: string) => { + setTab(tab); + }; return (
    - {user &&
  • Your Feed
  • } -
  • Global Feed
  • - {tag &&
  • # {tag}
  • } + {email && ( +
  • handleTabClick('your')} + > + Your Feed +
  • + )} +
  • handleTabClick('global')} + > + Global Feed +
  • + {tab !== 'global' && tab !== 'your' &&
  • # {tab}
  • }
); }; diff --git a/components/comments/CommentBox.tsx b/components/comments/CommentBox.tsx new file mode 100644 index 00000000..9ec98675 --- /dev/null +++ b/components/comments/CommentBox.tsx @@ -0,0 +1,27 @@ +'use client'; +import useUserStore from '@/stores/useUserStore'; +import Link from 'next/link'; +import React from 'react'; +import CommentForm from './CommentForm'; +import CommentCard from './CommentCard'; +import { flexCenter, flexRow, textCenter } from '@/styles/common.css'; + +const CommentBox = () => { + const { email } = useUserStore(); + return ( +
+ {email ? ( +
+ + +
+ ) : ( +
+ Sign in or sign up to add comments on this article. +
+ )} +
+ ); +}; + +export default CommentBox; diff --git a/components/tags/TagList.tsx b/components/tags/TagList.tsx index 212e9eb0..170c6771 100644 --- a/components/tags/TagList.tsx +++ b/components/tags/TagList.tsx @@ -1,18 +1,16 @@ 'use client'; import { tagList } from '@/styles/layout.css'; import Tag from './Tag'; -import { useArticlesByTag } from '@/hooks/useArticles'; -import useCurrentTag from '@/stores/useCurrentTag'; +import useCurrentTab from '@/stores/useCurrentTab'; type Props = { tags: string[]; }; const TagList = ({ tags }: Props) => { - const { tag, setTag } = useCurrentTag(); - const { data } = useArticlesByTag(tag); + const { setTab } = useCurrentTab(); const handleTagClick = (tag: string) => { - setTag(tag); + setTab(tag); }; return (
    {tags?.map((tag, index) => )}
diff --git a/hooks/useArticles.ts b/hooks/useArticles.ts index cb4d3a38..26719530 100644 --- a/hooks/useArticles.ts +++ b/hooks/useArticles.ts @@ -1,10 +1,42 @@ -import { getArticlesWithTagAPI } from '@/services/articles'; -import { useQuery } from '@tanstack/react-query'; - -export const useArticlesByTag = (tag: string) => { - return useQuery({ - queryKey: ['articles-tag', tag], - queryFn: () => getArticlesWithTagAPI(tag), - enabled: !!tag, +import { getArticlesAPI, getArticlesWithTagAPI } from '@/services/articles'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { RefObject } from 'react'; +import useIntersectionObserver from './useIntersectionObserver'; + +const useArticles = (ref: RefObject, tab: string) => { + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ + queryKey: ['articles', tab], + queryFn: ({ pageParam = 0 }) => { + switch (tab) { + case 'global': + return getArticlesAPI(pageParam); + case 'your': + return fetch(`/api/articles/feed?page=${pageParam}`).then(res => res.json()); + default: + return getArticlesWithTagAPI(tab, pageParam); + } + }, + getNextPageParam: (lastPage, pages) => { + const totalPage = Math.ceil(lastPage.articlesCount / 10); + let currentPage = pages.length; + if (lastPage.articles === 0 || totalPage < pages.length) { + return undefined; + } + + return currentPage++; + }, }); + + const nextPage = () => { + if (!hasNextPage || isFetchingNextPage) { + return; + } + fetchNextPage(); + }; + + useIntersectionObserver(nextPage, ref); + + return { data }; }; + +export default useArticles; diff --git a/middleware.ts b/middleware.ts index 65574885..e5b165be 100644 --- a/middleware.ts +++ b/middleware.ts @@ -6,9 +6,9 @@ export async function middleware(request: NextRequest) { const token = request.cookies.get('token')?.value || ''; - // if (path === '/api/user'){ - - // } + if (path.includes('/api/articles/favorite') && !token) { + return new NextResponse('Authentication Error', { status: 401 }); + } if (isPublic && token) { return NextResponse.redirect(new URL('/', request.nextUrl)); @@ -20,5 +20,5 @@ export async function middleware(request: NextRequest) { } export const config = { - matcher: ['/settings', '/editor', '/login', '/register', '/api/user'], + matcher: ['/settings', '/editor', '/login', '/register', '/api/user', '/api/articles/favorite/:path*'], }; diff --git a/services/articles.ts b/services/articles.ts index 5f6cdd5c..95dc38fc 100644 --- a/services/articles.ts +++ b/services/articles.ts @@ -8,12 +8,42 @@ const getArticlesWithTagAPI = (tag: string, offset = 0, limit = 10) => { return http.get(`/articles?tag=${tag}&limit=${limit}&offset=${offset ? offset * limit : 0}`); }; -const favoriteAPI = (slug: string) => { - return http.post(`/articles/${slug}/favorite`); +const getArticlesFeed = (offset = 0, auth: string, limit = 10) => { + console.log('Feed'); + + console.log(auth); + + return http.get(`/articles/feed?limit=${limit}&offset=${offset ? offset * limit : 0}`, { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Token ${auth}`, + }, + }); +}; + +const getArticleAPI = (slug: string) => { + return http.get(`/articles/${slug}`); +}; + +const favoriteAPI = (slug: string, auth: string) => { + console.log(slug); + console.log(auth); + + return http.post(`/articles/${slug}/favorite`, '', { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Token ${auth}`, + }, + }); }; -const unFavoriteAPI = (slug: string) => { - return http.delete(`/articles/${slug}/favorite`); +const unFavoriteAPI = (slug: string, auth: string) => { + return http.delete(`/articles/${slug}/favorite`, { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Token ${auth}`, + }, + }); }; -export { getArticlesAPI, getArticlesWithTagAPI, favoriteAPI, unFavoriteAPI }; +export { getArticlesAPI, getArticlesWithTagAPI, getArticlesFeed, getArticleAPI, favoriteAPI, unFavoriteAPI }; diff --git a/stores/useCurrentTab.ts b/stores/useCurrentTab.ts new file mode 100644 index 00000000..aecae23c --- /dev/null +++ b/stores/useCurrentTab.ts @@ -0,0 +1,8 @@ +import { create } from 'zustand'; + +const useCurrentTab = create(set => ({ + tab: 'global', + setTab: (tab: string) => set(() => ({ tab })), +})); + +export default useCurrentTab; diff --git a/stores/useCurrentTag.ts b/stores/useCurrentTag.ts deleted file mode 100644 index 2e2f81ac..00000000 --- a/stores/useCurrentTag.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { create } from 'zustand'; - -const useCurrentTag = create(set => ({ - tag: '', - setTag: (tag: string) => set(() => ({ tag })), -})); - -export default useCurrentTag; diff --git a/utils/http.ts b/utils/http.ts index 4837e245..a585fcf1 100644 --- a/utils/http.ts +++ b/utils/http.ts @@ -9,6 +9,8 @@ export const http = { body: body ? JSON.stringify(body) : undefined, ...options, }; + console.log(defaultOptions); + // console.log('서버자나'); // console.log(API_BASE_URL); From 77d971849b264ab50b39def9b927726867080d3e Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Fri, 29 Sep 2023 11:38:34 +0900 Subject: [PATCH 17/49] =?UTF-8?q?fix:=20useArticles=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EA=B0=80=20=EC=A0=81=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EC=83=9D=EA=B8=B0=EB=8A=94=20=EC=97=90=EB=9F=AC=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hooks/useArticles.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hooks/useArticles.ts b/hooks/useArticles.ts index 26719530..856aecfe 100644 --- a/hooks/useArticles.ts +++ b/hooks/useArticles.ts @@ -6,20 +6,20 @@ import useIntersectionObserver from './useIntersectionObserver'; const useArticles = (ref: RefObject, tab: string) => { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ queryKey: ['articles', tab], - queryFn: ({ pageParam = 0 }) => { + queryFn: async ({ pageParam = 0 }) => { switch (tab) { case 'global': - return getArticlesAPI(pageParam); + return await getArticlesAPI(pageParam); case 'your': return fetch(`/api/articles/feed?page=${pageParam}`).then(res => res.json()); default: - return getArticlesWithTagAPI(tab, pageParam); + return await getArticlesWithTagAPI(tab, pageParam); } }, getNextPageParam: (lastPage, pages) => { const totalPage = Math.ceil(lastPage.articlesCount / 10); let currentPage = pages.length; - if (lastPage.articles === 0 || totalPage < pages.length) { + if (lastPage.articlesCount < 11 || totalPage < pages.length) { return undefined; } From 6f9e355fbeefab0da268765e41d6954c60d29fc7 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Fri, 29 Sep 2023 13:58:18 +0900 Subject: [PATCH 18/49] =?UTF-8?q?feat:=20follow=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/articles/feed/route.ts | 3 --- app/api/profiles/[username]/route.ts | 40 ++++++++++++++++++++++++++++ app/api/profiles/route.ts | 11 -------- app/api/user/route.ts | 7 ++--- app/article/[slug]/page.tsx | 1 + components/user/FollowButton.tsx | 19 ++++++++++--- middleware.ts | 12 +++++++-- services/profile.ts | 10 +------ utils/http.ts | 3 ++- 9 files changed, 72 insertions(+), 34 deletions(-) create mode 100644 app/api/profiles/[username]/route.ts delete mode 100644 app/api/profiles/route.ts diff --git a/app/api/articles/feed/route.ts b/app/api/articles/feed/route.ts index 719127ea..db3baf6d 100644 --- a/app/api/articles/feed/route.ts +++ b/app/api/articles/feed/route.ts @@ -7,11 +7,8 @@ export async function GET(request: NextRequest) { const page = searchParams.get('page'); const token = request.cookies.get('token')?.value || ''; - console.log(token); const { articles, articlesCount } = await getArticlesFeed(Number(page), token); - console.log(articles); - console.log(articlesCount); return NextResponse.json({ articles, articlesCount }); } diff --git a/app/api/profiles/[username]/route.ts b/app/api/profiles/[username]/route.ts new file mode 100644 index 00000000..c97987cc --- /dev/null +++ b/app/api/profiles/[username]/route.ts @@ -0,0 +1,40 @@ +import { http } from '@/utils/http'; +import { NextRequest, NextResponse } from 'next/server'; + +// Follow a user +async function POST(req: NextRequest, route: { params: { username: string } }) { + const username = route.params.username; + + const token = req.cookies.get('token')?.value || ''; + + try { + const response = await http.post(`/profiles/${username}/follow`, '', { + headers: { + Authorization: `Token ${token}`, + }, + }); + + return NextResponse.json({ message: 'Follow Success', response }); + } catch (err) { + return NextResponse.json({ message: 'Follow Fail', err }); + } +} + +// Unfollow a user +async function DELETE(req: NextRequest, route: { params: { username: string } }) { + const username = route.params.username; + const token = req.cookies.get('token')?.value || ''; + try { + const response = await http.delete(`/profiles/${username}/follow`, { + headers: { + Authorization: `Token ${token}`, + }, + }); + + return NextResponse.json({ message: 'Unfollow Success', response }); + } catch (err) { + return NextResponse.json({ message: 'Unfollow Fail', err }); + } +} + +export { POST, DELETE }; diff --git a/app/api/profiles/route.ts b/app/api/profiles/route.ts deleted file mode 100644 index 38300314..00000000 --- a/app/api/profiles/route.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NextRequest } from 'next/server'; - -function GET(req: NextRequest) { - const username = req.nextUrl.searchParams; - const url = req.nextUrl; - console.log(username); - - console.log(url); -} - -export { GET }; diff --git a/app/api/user/route.ts b/app/api/user/route.ts index db999ac0..9e8cda77 100644 --- a/app/api/user/route.ts +++ b/app/api/user/route.ts @@ -3,10 +3,8 @@ import { getUserAPI, updateUserAPI } from '@/services/users'; export async function GET(req: NextRequest) { const { value } = req.cookies.get('token'); - console.log(value); - console.log('route user get'); + const { user } = await getUserAPI(value); - console.log(user); return NextResponse.json({ message: 'Login successfull', @@ -18,7 +16,6 @@ export async function GET(req: NextRequest) { export async function PUT(req: NextRequest) { const { value } = req.cookies.get('token'); const user = await req.json(); - console.log(user); - console.log('route user put'); + return updateUserAPI(user, value); } diff --git a/app/article/[slug]/page.tsx b/app/article/[slug]/page.tsx index e378a511..60659f90 100644 --- a/app/article/[slug]/page.tsx +++ b/app/article/[slug]/page.tsx @@ -22,6 +22,7 @@ const ArticlePage = async ({ params: { slug } }: Props) => {

{title}

+
diff --git a/components/user/FollowButton.tsx b/components/user/FollowButton.tsx index bf0863e0..65fc74fa 100644 --- a/components/user/FollowButton.tsx +++ b/components/user/FollowButton.tsx @@ -2,14 +2,27 @@ import Button from '@/composables/Button'; import { PlusIcon } from '@/composables/icons'; import { fontSize } from '@/styles/common.css'; +import { useMutation } from '@tanstack/react-query'; +import { useRouter } from 'next/navigation'; type Props = { author: any; }; const FollowButton = ({ author }: Props) => { + const router = useRouter(); + const { mutate } = useMutation({ + mutationFn: () => fetch(`/api/profiles/${author.username}`, { method: 'POST' }).then(res => res.json()), + onSuccess: data => { + console.log(data); + }, + onError: error => { + console.error(error); + router.push('/login'); + }, + }); + return ( - ); }; diff --git a/middleware.ts b/middleware.ts index e5b165be..49573027 100644 --- a/middleware.ts +++ b/middleware.ts @@ -6,7 +6,7 @@ export async function middleware(request: NextRequest) { const token = request.cookies.get('token')?.value || ''; - if (path.includes('/api/articles/favorite') && !token) { + if (path.includes('/api') && !token) { return new NextResponse('Authentication Error', { status: 401 }); } @@ -20,5 +20,13 @@ export async function middleware(request: NextRequest) { } export const config = { - matcher: ['/settings', '/editor', '/login', '/register', '/api/user', '/api/articles/favorite/:path*'], + matcher: [ + '/settings', + '/editor', + '/login', + '/register', + '/api/user', + '/api/articles/favorite/:path*', + '/api/profiles', + ], }; diff --git a/services/profile.ts b/services/profile.ts index e4a0e4d9..69fd20f8 100644 --- a/services/profile.ts +++ b/services/profile.ts @@ -4,12 +4,4 @@ const getProfile = (username: string) => { return http.get(`/profiles/${username}`); }; -const followUser = (username: string) => { - return http.post(`/profiles/${username}`); -}; - -const unFollowUser = (username: string) => { - return http.delete(`/profiles/${username}`); -}; - -export { getProfile, followUser, unFollowUser }; +export { getProfile }; diff --git a/utils/http.ts b/utils/http.ts index a585fcf1..2e2fb279 100644 --- a/utils/http.ts +++ b/utils/http.ts @@ -28,7 +28,8 @@ export const http = { throw new Error(errorData.message || 'Request failed'); } - // console.log(response); + console.log(response); + console.log(JSON.stringify(response)); console.log('성공'); From d47e250d8c91c2fc67656c74c94fa8aa6f3a934d Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Fri, 29 Sep 2023 14:42:13 +0900 Subject: [PATCH 19/49] =?UTF-8?q?feat:=20Banner=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20login=20=ED=96=88=EC=9D=84=20=EB=95=8C=20h?= =?UTF-8?q?ome=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20=EC=95=88=EB=B3=B4?= =?UTF-8?q?=EC=9D=B4=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/article/[slug]/page.tsx | 3 ++- app/page.tsx | 9 ++++++--- components/layouts/Banner.tsx | 7 ++++++- components/user/FollowButton.tsx | 14 ++++++++++---- styles/home.css.ts | 2 -- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/article/[slug]/page.tsx b/app/article/[slug]/page.tsx index 60659f90..1c139040 100644 --- a/app/article/[slug]/page.tsx +++ b/app/article/[slug]/page.tsx @@ -13,8 +13,10 @@ type Props = { }; const ArticlePage = async ({ params: { slug } }: Props) => { const { + article, article: { title, author, createdAt, body, tagList, favoritesCount }, } = await getArticleAPI(slug); + console.log(article); return (
@@ -22,7 +24,6 @@ const ArticlePage = async ({ params: { slug } }: Props) => {

{title}

-
diff --git a/app/page.tsx b/app/page.tsx index a1b4e424..08adea15 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,8 +1,9 @@ import ArticleList from '@/components/article/ArticleList'; import Banner from '@/components/layouts/Banner'; import SideBar from '@/components/layouts/SideBar'; + import { articleContainer } from '@/styles/article.css'; -import { container, flex } from '@/styles/common.css'; +import { container, flex, textCenter } from '@/styles/common.css'; import { bannerDescription, bannerTitle } from '@/styles/home.css'; import dynamic from 'next/dynamic'; import { Suspense } from 'react'; @@ -13,8 +14,10 @@ export default function Page() { return (
-

conduit

-

A place to share your knowledge.

+
+

conduit

+

A place to share your knowledge.

+
diff --git a/components/layouts/Banner.tsx b/components/layouts/Banner.tsx index aeaaa88a..fc341deb 100644 --- a/components/layouts/Banner.tsx +++ b/components/layouts/Banner.tsx @@ -1,3 +1,5 @@ +'use client'; +import useUserStore from '@/stores/useUserStore'; import { backgroundBlack, backgroundGreen, container } from '@/styles/common.css'; import { banner } from '@/styles/home.css'; import { ReactNode } from 'react'; @@ -6,7 +8,10 @@ type Props = { background: 'green' | 'black'; }; const Banner = ({ children, background }: Props) => { - return ( + const { email } = useUserStore(); + return email && background === 'green' ? ( + <> + ) : (
{children}
diff --git a/components/user/FollowButton.tsx b/components/user/FollowButton.tsx index 65fc74fa..f33f92b7 100644 --- a/components/user/FollowButton.tsx +++ b/components/user/FollowButton.tsx @@ -2,16 +2,22 @@ import Button from '@/composables/Button'; import { PlusIcon } from '@/composables/icons'; import { fontSize } from '@/styles/common.css'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; type Props = { author: any; }; -const FollowButton = ({ author }: Props) => { +const FollowButton = ({ author: { username, following } }: Props) => { const router = useRouter(); + const queryClient = useQueryClient(); + const { mutate } = useMutation({ - mutationFn: () => fetch(`/api/profiles/${author.username}`, { method: 'POST' }).then(res => res.json()), + mutationFn: async () => { + const method = following ? 'DELETE' : 'POST'; + return fetch(`/api/profiles/${username}`, { method }).then(res => res.json()); + }, onSuccess: data => { + queryClient.invalidateQueries(['articles', 'global']); console.log(data); }, onError: error => { @@ -22,7 +28,7 @@ const FollowButton = ({ author }: Props) => { return ( ); }; diff --git a/styles/home.css.ts b/styles/home.css.ts index fd8e1213..c3bda877 100644 --- a/styles/home.css.ts +++ b/styles/home.css.ts @@ -1,8 +1,6 @@ import { style } from '@vanilla-extract/css'; -import { textCenter } from './common.css'; export const banner = style([ - textCenter, { padding: '2rem', color: '#fff', From b7d349543a77a3ca8bab42f173edbcb816f7278b Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Sun, 1 Oct 2023 23:39:58 +0900 Subject: [PATCH 20/49] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/articles/favorite/[slug]/route.ts | 27 ++++++++++++----- app/api/articles/new/route.ts | 27 +++++++++++++++++ app/editor/page.tsx | 36 ++++++++++++++++------- components/article/ArticleList.tsx | 2 +- components/article/ArticlePreview.tsx | 16 ++++++++-- hooks/useArticles.ts | 18 ++++++++++-- services/articles.ts | 23 +-------------- 7 files changed, 104 insertions(+), 45 deletions(-) create mode 100644 app/api/articles/new/route.ts diff --git a/app/api/articles/favorite/[slug]/route.ts b/app/api/articles/favorite/[slug]/route.ts index 538d4571..f9a27b50 100644 --- a/app/api/articles/favorite/[slug]/route.ts +++ b/app/api/articles/favorite/[slug]/route.ts @@ -1,15 +1,22 @@ -import { favoriteAPI, unFavoriteAPI } from '@/services/articles'; +import { http } from '@/utils/http'; import { NextRequest, NextResponse } from 'next/server'; +// 좋아요 export async function POST(request: NextRequest, route: { params: { slug: string } }) { try { - console.log('좋아요'); - - console.log(route.params.slug); - const slug = route.params.slug; const token = request.cookies.get('token')?.value || ''; - const res = await favoriteAPI(slug, token); + console.log('좋아요 Route'); + console.log(slug); + console.log(token); + + const res = await http.post(`/articles/${slug}/favorite`, '', { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Token ${token}`, + }, + }); + console.log(res); return NextResponse.json({ message: 'Favorite Success', success: true, data: res }); @@ -18,12 +25,18 @@ export async function POST(request: NextRequest, route: { params: { slug: string } } +// 좋아요 취소 export async function DELETE(request: NextRequest) { try { console.log('좋아요 취소'); const slug = await request.json(); const token = request.cookies.get('token')?.value || ''; - const res = await unFavoriteAPI(slug, token); + const res = await http.delete(`/articles/${slug}/favorite`, { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Token ${token}`, + }, + }); return NextResponse.json({ message: 'Un Favorite Success', success: true, data: res }); } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 400 }); diff --git a/app/api/articles/new/route.ts b/app/api/articles/new/route.ts new file mode 100644 index 00000000..aeadf2c3 --- /dev/null +++ b/app/api/articles/new/route.ts @@ -0,0 +1,27 @@ +import { http } from '@/utils/http'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + try { + const token = request.cookies.get('token')?.value || ''; + const body = await request.json(); + + console.log('Create Article'); + + console.log(token); + console.log(body); + + const res = await http.post('/articles', body, { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Token ${token}`, + }, + }); + + console.log(res); + + return NextResponse.json({ message: 'Create Article Success', success: true, data: res }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } +} diff --git a/app/editor/page.tsx b/app/editor/page.tsx index cf5adbba..8f81d10e 100644 --- a/app/editor/page.tsx +++ b/app/editor/page.tsx @@ -1,13 +1,14 @@ 'use client'; import TagInput from '@/components/editor/TagInput'; -import { registerArticle } from '@/services/articles'; import { articleTextarea } from '@/styles/article.css'; -import { commentTextarea } from '@/styles/comments.css'; import { container, input } from '@/styles/common.css'; import { editorForm, editorButton } from '@/styles/editor.css'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import React, { useState } from 'react'; const EditorPage = () => { + const queryClient = useQueryClient(); + const [formData, setFormData] = useState({ title: '', description: '', @@ -15,12 +16,25 @@ const EditorPage = () => { tagList: [], }); - const handleSubmit = async (e: any) => { - e.preventDefault(); - // console.log('들어옴'); + const { mutate } = useMutation({ + mutationFn: () => + fetch('/api/articles/new', { method: 'POST', body: JSON.stringify({ article: formData }) }).then(res => + res.json() + ), + onSuccess: data => { + console.log('등록 성공'); + + console.log(data); + queryClient.invalidateQueries(['articles', 'global']); + }, + onError: (error: any) => { + console.log('에러 발생'); + console.error(error); + }, + }); - // const res = await registerArticle(); - // console.log(res); + const handleClick = () => { + mutate(); }; const handleChange = (e: any) => { @@ -31,7 +45,7 @@ const EditorPage = () => { }; return (
-
+
{ >
- +
- +
); }; diff --git a/components/article/ArticleList.tsx b/components/article/ArticleList.tsx index 15a07dfc..5b9dddee 100644 --- a/components/article/ArticleList.tsx +++ b/components/article/ArticleList.tsx @@ -13,7 +13,7 @@ const ArticleList = () => { return (
- {data?.pages?.at(-1)?.articles?.length === 0 ? ( + {data?.pages?.at(0)?.articles?.length === 0 ? ( '데이터가 없습니다.' ) : (
diff --git a/components/article/ArticlePreview.tsx b/components/article/ArticlePreview.tsx index a89d2336..71a2e50c 100644 --- a/components/article/ArticlePreview.tsx +++ b/components/article/ArticlePreview.tsx @@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation'; import { FillHeartIcon } from '@/composables/icons'; import { fillGreenButton, flex, flexBetween, greenButton } from '@/styles/common.css'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import useCurrentTab from '@/stores/useCurrentTab'; type Props = { article: any; @@ -14,6 +15,7 @@ const ArticlePreview = ({ article: { title, description, favorited, favoritesCount, tagList, author, createdAt, slug }, }: Props) => { const router = useRouter(); + const { tab } = useCurrentTab(); const queryClient = useQueryClient(); @@ -26,12 +28,20 @@ const ArticlePreview = ({ }, onError: err => { // 권한이 없을 경우 login 페이지로 이동 + console.log(err); + router.push('/login'); }, - onSuccess: res => { + onSuccess: data => { console.log('성공 후 리패치'); - - queryClient.invalidateQueries({ queryKey: ['articles'] }); + console.log(data); + // queryClient.invalidateQueries({ queryKey: ['articles', tab] }); + queryClient.setQueryData(['articles', tab], (oldQueryData: any) => { + return { + ...oldQueryData, + data: [...oldQueryData.data, data?.data], + }; + }); }, }); return ( diff --git a/hooks/useArticles.ts b/hooks/useArticles.ts index 856aecfe..d6ceef7c 100644 --- a/hooks/useArticles.ts +++ b/hooks/useArticles.ts @@ -1,5 +1,5 @@ import { getArticlesAPI, getArticlesWithTagAPI } from '@/services/articles'; -import { useInfiniteQuery } from '@tanstack/react-query'; +import { useInfiniteQuery, useMutation } from '@tanstack/react-query'; import { RefObject } from 'react'; import useIntersectionObserver from './useIntersectionObserver'; @@ -27,6 +27,20 @@ const useArticles = (ref: RefObject, tab: string) => { }, }); + const { mutate } = useMutation({ + mutationFn: async (slug: string) => { + return fetch(`/api/articles/favorite/${slug}`).then(res => res.json()); + }, + onSuccess: data => { + console.log('성공'); + console.log(data); + }, + onError: err => { + console.log('Error 발생'); + console.log(err); + }, + }); + const nextPage = () => { if (!hasNextPage || isFetchingNextPage) { return; @@ -36,7 +50,7 @@ const useArticles = (ref: RefObject, tab: string) => { useIntersectionObserver(nextPage, ref); - return { data }; + return { data, mutate }; }; export default useArticles; diff --git a/services/articles.ts b/services/articles.ts index 95dc38fc..bdccdd4f 100644 --- a/services/articles.ts +++ b/services/articles.ts @@ -25,25 +25,4 @@ const getArticleAPI = (slug: string) => { return http.get(`/articles/${slug}`); }; -const favoriteAPI = (slug: string, auth: string) => { - console.log(slug); - console.log(auth); - - return http.post(`/articles/${slug}/favorite`, '', { - headers: { - 'Content-Type': 'application/json; charset=utf-8', - Authorization: `Token ${auth}`, - }, - }); -}; - -const unFavoriteAPI = (slug: string, auth: string) => { - return http.delete(`/articles/${slug}/favorite`, { - headers: { - 'Content-Type': 'application/json; charset=utf-8', - Authorization: `Token ${auth}`, - }, - }); -}; - -export { getArticlesAPI, getArticlesWithTagAPI, getArticlesFeed, getArticleAPI, favoriteAPI, unFavoriteAPI }; +export { getArticlesAPI, getArticlesWithTagAPI, getArticlesFeed, getArticleAPI }; From 12f44e4523aa160fd5ee72dcf0e0f3e7fd93f3df Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Wed, 4 Oct 2023 20:10:55 +0900 Subject: [PATCH 21/49] =?UTF-8?q?feat:=20getArticlesWithAuthorAPI,=20getAr?= =?UTF-8?q?ticlesWithFavoritedAPI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/page.tsx | 3 ++- components/layouts/Banner.tsx | 2 +- services/articles.ts | 19 +++++++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 08adea15..6028fb69 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,5 @@ import ArticleList from '@/components/article/ArticleList'; -import Banner from '@/components/layouts/Banner'; + import SideBar from '@/components/layouts/SideBar'; import { articleContainer } from '@/styles/article.css'; @@ -9,6 +9,7 @@ import dynamic from 'next/dynamic'; import { Suspense } from 'react'; const ArticleTab = dynamic(() => import('@/components/article/ArticleTab'), { ssr: false }); +const Banner = dynamic(() => import('@/components/layouts/Banner'), { ssr: false }); export default function Page() { return ( diff --git a/components/layouts/Banner.tsx b/components/layouts/Banner.tsx index fc341deb..2c7ff73b 100644 --- a/components/layouts/Banner.tsx +++ b/components/layouts/Banner.tsx @@ -10,7 +10,7 @@ type Props = { const Banner = ({ children, background }: Props) => { const { email } = useUserStore(); return email && background === 'green' ? ( - <> +
) : (
{children}
diff --git a/services/articles.ts b/services/articles.ts index bdccdd4f..55f05b8b 100644 --- a/services/articles.ts +++ b/services/articles.ts @@ -1,6 +1,6 @@ import { http } from '@/utils/http'; -const getArticlesAPI = (offset = 0, limit = 10) => { +const getArticlesAPI = (offset = 0, limit = 20) => { return http.get(`/articles?limit=${limit}&offset=${offset ? offset * limit : 0}`); }; @@ -8,6 +8,14 @@ const getArticlesWithTagAPI = (tag: string, offset = 0, limit = 10) => { return http.get(`/articles?tag=${tag}&limit=${limit}&offset=${offset ? offset * limit : 0}`); }; +const getArticlesWithAuthorAPI = (username: string, offset = 0, limit = 10) => { + return http.get(`/articles?author=${username}&limit=${limit}&offset=${offset ? offset * limit : 0}`); +}; + +const getArticlesWithFavoritedAPI = (username: string, offset = 0, limit = 10) => { + return http.get(`/articles?favorited=${username}&limit=${limit}&offset=${offset ? offset * limit : 0}`); +}; + const getArticlesFeed = (offset = 0, auth: string, limit = 10) => { console.log('Feed'); @@ -25,4 +33,11 @@ const getArticleAPI = (slug: string) => { return http.get(`/articles/${slug}`); }; -export { getArticlesAPI, getArticlesWithTagAPI, getArticlesFeed, getArticleAPI }; +export { + getArticlesAPI, + getArticlesWithAuthorAPI, + getArticlesWithFavoritedAPI, + getArticlesWithTagAPI, + getArticlesFeed, + getArticleAPI, +}; From be022734eaed778c454151e312ff4cc909d9972f Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Wed, 4 Oct 2023 20:31:58 +0900 Subject: [PATCH 22/49] =?UTF-8?q?ffeat:=20TagList=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20useCurrentTab=20=ED=83=80=EC=9E=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/editor/TagInput.tsx | 6 ++++-- components/layouts/SideBar.tsx | 23 ++++++++++++++++++----- components/tags/TagList.tsx | 13 +++---------- services/tags.ts | 5 +++++ stores/useCurrentTab.ts | 7 ++++++- 5 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 services/tags.ts diff --git a/components/editor/TagInput.tsx b/components/editor/TagInput.tsx index d3cc7c1f..c0ce8980 100644 --- a/components/editor/TagInput.tsx +++ b/components/editor/TagInput.tsx @@ -20,7 +20,9 @@ const TagInput = ({ setFormData }: Props) => { } }; - const handleClick = (tag: string) => { + const handleTagClick = (tag: string) => { + console.log('쿨릭'); + setTags((prevTags: string[]) => prevTags.filter(prevTag => prevTag !== tag)); }; @@ -34,7 +36,7 @@ const TagInput = ({ setFormData }: Props) => { onKeyDown={handleKeyDown} className={input} /> - + ); }; diff --git a/components/layouts/SideBar.tsx b/components/layouts/SideBar.tsx index b0207c72..bc87efe2 100644 --- a/components/layouts/SideBar.tsx +++ b/components/layouts/SideBar.tsx @@ -1,16 +1,29 @@ -import React from 'react'; +'use client'; import TagList from '../tags/TagList'; import { sideBar, sideBarText } from '@/styles/layout.css'; import { sidePadding } from '@/styles/common.css'; -import { http } from '@/utils/http'; -const SideBar = async () => { - const { tags } = await http.get('/tags'); +import useCurrentTab from '@/stores/useCurrentTab'; +import { useQuery } from '@tanstack/react-query'; +import { getTags } from '@/services/tags'; + +const SideBar = () => { + const { data: tags } = useQuery({ + queryKey: ['tags'], + queryFn: getTags, + select: res => res.tags, + }); + + const { setTab } = useCurrentTab(); + + const handleTagClick = (tag: string) => { + setTab(tag); + }; return (

Popular Tags

- +
); diff --git a/components/tags/TagList.tsx b/components/tags/TagList.tsx index 170c6771..ab8a2811 100644 --- a/components/tags/TagList.tsx +++ b/components/tags/TagList.tsx @@ -1,20 +1,13 @@ 'use client'; import { tagList } from '@/styles/layout.css'; import Tag from './Tag'; -import useCurrentTab from '@/stores/useCurrentTab'; type Props = { tags: string[]; + onClick: (tag: string) => void; }; -const TagList = ({ tags }: Props) => { - const { setTab } = useCurrentTab(); - - const handleTagClick = (tag: string) => { - setTab(tag); - }; - return ( -
    {tags?.map((tag, index) => )}
- ); +const TagList = ({ tags, onClick }: Props) => { + return
    {tags?.map((tag, index) => )}
; }; export default TagList; diff --git a/services/tags.ts b/services/tags.ts new file mode 100644 index 00000000..9af92b31 --- /dev/null +++ b/services/tags.ts @@ -0,0 +1,5 @@ +import { http } from '@/utils/http'; + +export const getTags = async () => { + return http.get('/tags'); +}; diff --git a/stores/useCurrentTab.ts b/stores/useCurrentTab.ts index aecae23c..87b154ea 100644 --- a/stores/useCurrentTab.ts +++ b/stores/useCurrentTab.ts @@ -1,6 +1,11 @@ import { create } from 'zustand'; -const useCurrentTab = create(set => ({ +type CurrentTabState = { + tab: string; + setTab: (tab: string) => void; +}; + +const useCurrentTab = create(set => ({ tab: 'global', setTab: (tab: string) => set(() => ({ tab })), })); From a7614492f21457721028fbf37d3a24518ee66304 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Wed, 4 Oct 2023 21:53:13 +0900 Subject: [PATCH 23/49] =?UTF-8?q?fix:=20useUserStore=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/login/page.tsx | 4 ++-- app/register/page.tsx | 4 ++-- app/settings/page.tsx | 3 ++- components/article/ArticleTab.tsx | 3 ++- components/comments/CommentBox.tsx | 3 ++- components/layouts/Banner.tsx | 3 ++- components/layouts/NavBar.tsx | 3 ++- types/index.ts | 20 ++++++++++---------- 8 files changed, 24 insertions(+), 19 deletions(-) diff --git a/app/login/page.tsx b/app/login/page.tsx index 61b9640e..ad9e84e9 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -4,14 +4,14 @@ import useUserStore from '@/stores/useUserStore'; import { form, question, title } from '@/styles/account.css'; import { input, container, flexCenter, flexRow, fillGreenButton } from '@/styles/common.css'; import { buttonBox } from '@/styles/layout.css'; -import { LoginUser } from '@/types'; +import { LoginUser, UserAction } from '@/types'; import { useMutation } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import { ChangeEvent, FormEvent, useState } from 'react'; const LoginPage = () => { const router = useRouter(); - const { login } = useUserStore(); + const { login } = useUserStore() as UserAction; const [formData, setFormData] = useState({ email: '', diff --git a/app/register/page.tsx b/app/register/page.tsx index 8ab918a6..f060dae9 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -4,7 +4,7 @@ import useUserStore from '@/stores/useUserStore'; import { form, question, title } from '@/styles/account.css'; import { input, container, flexRow, flexCenter, fillGreenButton } from '@/styles/common.css'; import { buttonBox } from '@/styles/layout.css'; -import { NewUser } from '@/types'; +import { NewUser, UserAction } from '@/types'; import { useMutation } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; @@ -12,7 +12,7 @@ import { ChangeEvent, FormEvent, useState } from 'react'; const RegisterPage = () => { const router = useRouter(); - const { login } = useUserStore(); + const { login } = useUserStore() as UserAction; const [formData, setFormData] = useState({ username: '', email: '', diff --git a/app/settings/page.tsx b/app/settings/page.tsx index b8dc6a74..2f91e0d3 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -3,6 +3,7 @@ import useUserStore from '@/stores/useUserStore'; import { articleTextarea } from '@/styles/article.css'; import { container, flex, hr, input } from '@/styles/common.css'; import { logoutButton, settingBlock, settingForm, settingTitle, updateButton } from '@/styles/settings.css'; +import { User, UserAction } from '@/types'; import { useMutation } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; @@ -11,7 +12,7 @@ import { useState } from 'react'; const SettingsPage = () => { const router = useRouter(); - const { logout, email, username, image, bio } = useUserStore(); + const { logout, email, username, image, bio } = useUserStore() as User & UserAction; // 초기화 함수로 전환 const [formData, setFormData] = useState({ diff --git a/components/article/ArticleTab.tsx b/components/article/ArticleTab.tsx index 92167d50..4f4a4ba7 100644 --- a/components/article/ArticleTab.tsx +++ b/components/article/ArticleTab.tsx @@ -2,9 +2,10 @@ import useCurrentTab from '@/stores/useCurrentTab'; import useUserStore from '@/stores/useUserStore'; import { articleTab, articleTabItem, articleTabItemActivate, articleTabItemDisable } from '@/styles/article.css'; +import { User } from '@/types'; const ArticleTab = () => { - const { email } = useUserStore(); + const { email } = useUserStore() as User; const { tab, setTab } = useCurrentTab(); const handleTabClick = (tab: string) => { diff --git a/components/comments/CommentBox.tsx b/components/comments/CommentBox.tsx index 9ec98675..cdf8010e 100644 --- a/components/comments/CommentBox.tsx +++ b/components/comments/CommentBox.tsx @@ -5,9 +5,10 @@ import React from 'react'; import CommentForm from './CommentForm'; import CommentCard from './CommentCard'; import { flexCenter, flexRow, textCenter } from '@/styles/common.css'; +import { User } from '@/types'; const CommentBox = () => { - const { email } = useUserStore(); + const { email } = useUserStore() as User; return (
{email ? ( diff --git a/components/layouts/Banner.tsx b/components/layouts/Banner.tsx index 2c7ff73b..5f2c27dd 100644 --- a/components/layouts/Banner.tsx +++ b/components/layouts/Banner.tsx @@ -2,13 +2,14 @@ import useUserStore from '@/stores/useUserStore'; import { backgroundBlack, backgroundGreen, container } from '@/styles/common.css'; import { banner } from '@/styles/home.css'; +import { User } from '@/types'; import { ReactNode } from 'react'; type Props = { children: ReactNode; background: 'green' | 'black'; }; const Banner = ({ children, background }: Props) => { - const { email } = useUserStore(); + const { email } = useUserStore() as User; return email && background === 'green' ? (
) : ( diff --git a/components/layouts/NavBar.tsx b/components/layouts/NavBar.tsx index 24a7661a..6a14c5cc 100644 --- a/components/layouts/NavBar.tsx +++ b/components/layouts/NavBar.tsx @@ -6,6 +6,7 @@ import { userImageSm } from '@/styles/profile.css'; import Image from 'next/image'; import { EditIcon, SettingIcon } from '@/composables/icons'; import useUserStore from '@/stores/useUserStore'; +import { User } from '@/types'; const NAVS = [ { @@ -35,7 +36,7 @@ const NAVS = [ ]; const NavBar = () => { - const { username, image } = useUserStore(); + const { username, image } = useUserStore() as User; const pathname = usePathname(); return ( diff --git a/types/index.ts b/types/index.ts index 82b43efe..c3a2a83e 100644 --- a/types/index.ts +++ b/types/index.ts @@ -23,14 +23,6 @@ export type NewUser = { password: string; }; -export type User = { - email: string; - token?: string; - username: string; - bio: string; - image: string; -}; - export type UpdateUser = { email: string; password: string; @@ -80,9 +72,17 @@ export type Comment = { author: Profile; }; +export type User = { + email: string; + token?: string; + username: string; + bio: string; + image: string; +}; + export type UserAction = { - login?: (e: any) => void; - updateUser?: () => void; + login?: (user: User) => void; + updateUser?: (user: User) => void; logout?: () => void; reset?: () => void; }; From e140ded31c92a434f2288c32a77ddea5c448bd68 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Wed, 4 Oct 2023 23:50:15 +0900 Subject: [PATCH 24/49] =?UTF-8?q?fix:=20TagInput=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/editor/page.tsx | 13 +++++++++++-- components/editor/TagInput.tsx | 6 +++--- components/tags/Tag.tsx | 5 +++-- components/tags/TagList.tsx | 3 +-- types/index.ts | 2 +- utils/http.ts | 2 ++ 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/editor/page.tsx b/app/editor/page.tsx index 8f81d10e..453615d7 100644 --- a/app/editor/page.tsx +++ b/app/editor/page.tsx @@ -3,13 +3,16 @@ import TagInput from '@/components/editor/TagInput'; import { articleTextarea } from '@/styles/article.css'; import { container, input } from '@/styles/common.css'; import { editorForm, editorButton } from '@/styles/editor.css'; +import { NewArticle } from '@/types'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/navigation'; import React, { useState } from 'react'; const EditorPage = () => { const queryClient = useQueryClient(); + const router = useRouter(); - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ title: '', description: '', body: '', @@ -26,6 +29,7 @@ const EditorPage = () => { console.log(data); queryClient.invalidateQueries(['articles', 'global']); + router.push('router'); }, onError: (error: any) => { console.log('에러 발생'); @@ -43,6 +47,10 @@ const EditorPage = () => { [e.target.name]: e.target.value, })); }; + + const appendTag = (tag: string) => { + setFormData(prevData => ({ ...prevData, tagList: [...prevData.tagList, tag] })); + }; return (
@@ -62,6 +70,7 @@ const EditorPage = () => { onChange={handleChange} value={formData.description} /> + - +
); diff --git a/app/editor/[slug]/page.tsx b/app/editor/[slug]/page.tsx index 5d90da0e..854d3d93 100644 --- a/app/editor/[slug]/page.tsx +++ b/app/editor/[slug]/page.tsx @@ -1,7 +1,13 @@ -import React from 'react'; - -const EditorUpdatePage = () => { - return
EditorUpdatePage
; +import EditForm from '@/components/editor/EditForm'; +type Props = { + params: { slug: string }; +}; +const EditorUpdatePage = ({ params: { slug } }: Props) => { + return ( +
+ +
+ ); }; export default EditorUpdatePage; diff --git a/components/article/ArticleDeleteButton.tsx b/components/article/ArticleDeleteButton.tsx new file mode 100644 index 00000000..bbf0253c --- /dev/null +++ b/components/article/ArticleDeleteButton.tsx @@ -0,0 +1,12 @@ +import Button from '@/composables/Button'; + +const ArticleDeleteButton = () => { + const handleClick = () => {}; + return ( + + ); +}; + +export default ArticleDeleteButton; diff --git a/components/article/ArticleEditButton.tsx b/components/article/ArticleEditButton.tsx new file mode 100644 index 00000000..aee4c79a --- /dev/null +++ b/components/article/ArticleEditButton.tsx @@ -0,0 +1,17 @@ +'use client'; +import Button from '@/composables/Button'; +import { useRouter } from 'next/navigation'; + +const ArticleEditButton = ({ slug }: { slug: string }) => { + const router = useRouter(); + const handleButtonClick = () => { + router.push(`/editor/${slug}`); + }; + return ( + + ); +}; + +export default ArticleEditButton; diff --git a/components/editor/EditForm.tsx b/components/editor/EditForm.tsx new file mode 100644 index 00000000..b17b01fb --- /dev/null +++ b/components/editor/EditForm.tsx @@ -0,0 +1,100 @@ +'use client'; + +import { articleTextarea } from '@/styles/article.css'; +import { input } from '@/styles/common.css'; +import { editorButton, editorForm } from '@/styles/editor.css'; +import TagInput from './TagInput'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useState } from 'react'; +import { NewArticle } from '@/types'; +import { useRouter } from 'next/navigation'; +import { getArticleAPI } from '@/services/articles'; + +const EditForm = ({ slug }: { slug: string }) => { + const queryClient = useQueryClient(); + const router = useRouter(); + + const { data: article } = useQuery({ + queryKey: ['article', slug], + queryFn: async () => await getArticleAPI(slug), + enabled: !!slug, + select: res => res.article, + }); + + const [formData, setFormData] = useState({ + title: article ? article.title : '', + description: article ? article.description : '', + body: article ? article.body : '', + tagList: article ? [...article.tagList] : [], + }); + + const { mutate } = useMutation({ + mutationFn: () => + fetch('/api/articles/new', { method: 'POST', body: JSON.stringify({ article: formData }) }).then(res => + res.json() + ), + onSuccess: data => { + console.log('등록 성공'); + + console.log(data); + queryClient.invalidateQueries(['articles', 'global']); + router.push('router'); + }, + onError: (error: any) => { + console.log('에러 발생'); + console.error(error); + }, + }); + + const handleClick = () => { + mutate(); + }; + + const handleChange = (e: any) => { + setFormData(prev => ({ + ...prev, + [e.target.name]: e.target.value, + })); + }; + + const appendTag = (tag: string) => { + setFormData(prevData => ({ ...prevData, tagList: [...prevData.tagList, tag] })); + }; + return ( +
+ + + + + +
+ +
+
+ ); +}; + +export default EditForm; diff --git a/components/user/UserBox.tsx b/components/user/UserBox.tsx index ece87593..ca8813db 100644 --- a/components/user/UserBox.tsx +++ b/components/user/UserBox.tsx @@ -7,7 +7,10 @@ import Image from 'next/image'; import { useRouter } from 'next/navigation'; type Props = { - author: any; + author: { + username: string; + image: string; + }; createdAt: string; }; const UserBox = ({ author: { username, image }, createdAt }: Props) => { diff --git a/composables/Button.tsx b/composables/Button.tsx index 2bb3ec70..58abb5d4 100644 --- a/composables/Button.tsx +++ b/composables/Button.tsx @@ -1,6 +1,6 @@ 'use client'; -import { grayButton, greenButton } from '@/styles/common.css'; +import { grayButton, greenButton, redButton } from '@/styles/common.css'; import { Button } from '@/types'; const Button = ({ onClick, children, type }: Button) => { @@ -20,6 +20,9 @@ function getButtonStyle(type: string): React.JSX.Element { case 'gray': classType = grayButton; break; + case 'red': + classType = redButton; + break; } return classType; } From d10e622b940d9a88ead5813292248e806b6fbe5f Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Thu, 5 Oct 2023 05:33:49 +0900 Subject: [PATCH 31/49] =?UTF-8?q?feat:=20Article=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/articles/[slug]/route.ts | 52 ++++++++++++++++++++++ components/article/ArticleDeleteButton.tsx | 13 ++++-- 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 app/api/articles/[slug]/route.ts diff --git a/app/api/articles/[slug]/route.ts b/app/api/articles/[slug]/route.ts new file mode 100644 index 00000000..0b019bf2 --- /dev/null +++ b/app/api/articles/[slug]/route.ts @@ -0,0 +1,52 @@ +import { http } from '@/utils/http'; +import { NextRequest, NextResponse } from 'next/server'; + +async function GET(req: NextRequest, route: { params: { slug: string } }) {} +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 || ''; + console.log('좋아요 Route'); + console.log(slug); + console.log(token); + console.log(body); + + const res = await http.put(`/articles/${slug}`, body, { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Token ${token}`, + }, + }); + + console.log(res); + + 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 || ''; + console.log('좋아요 Route'); + console.log(slug); + console.log(token); + + const res = await http.delete(`/articles/${slug}`, { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Token ${token}`, + }, + }); + + console.log(res); + + 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 }; diff --git a/components/article/ArticleDeleteButton.tsx b/components/article/ArticleDeleteButton.tsx index bbf0253c..6a5eab6f 100644 --- a/components/article/ArticleDeleteButton.tsx +++ b/components/article/ArticleDeleteButton.tsx @@ -1,9 +1,16 @@ +'use client'; import Button from '@/composables/Button'; +import { useRouter } from 'next/navigation'; -const ArticleDeleteButton = () => { - const handleClick = () => {}; +const ArticleDeleteButton = ({ slug }: { slug: string }) => { + const router = useRouter(); + const handleButtonClick = async () => { + const res = fetch(`/api/articles/${slug}`, { method: 'DELETE' }).then(res => res.json()); + console.log(res); + router.push('/'); + }; return ( - ); From ef5e2b22d3db850b30691393bcda88bd822c604f Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Thu, 5 Oct 2023 05:43:53 +0900 Subject: [PATCH 32/49] =?UTF-8?q?feat:=20Article=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/editor/EditForm.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/components/editor/EditForm.tsx b/components/editor/EditForm.tsx index b17b01fb..daad4671 100644 --- a/components/editor/EditForm.tsx +++ b/components/editor/EditForm.tsx @@ -29,16 +29,23 @@ const EditForm = ({ slug }: { slug: string }) => { }); const { mutate } = useMutation({ - mutationFn: () => - fetch('/api/articles/new', { method: 'POST', body: JSON.stringify({ article: formData }) }).then(res => - res.json() - ), + mutationFn: async () => { + if (slug) { + return fetch(`/api/articles/${slug}`, { method: 'PUT', body: JSON.stringify({ article: formData }) }).then( + res => res.json() + ); + } else { + return fetch('/api/articles/new', { method: 'POST', body: JSON.stringify({ article: formData }) }).then(res => + res.json() + ); + } + }, onSuccess: data => { console.log('등록 성공'); console.log(data); queryClient.invalidateQueries(['articles', 'global']); - router.push('router'); + router.push('/'); }, onError: (error: any) => { console.log('에러 발생'); From 6aad78cb438d0e600a58b13db4d09058faa4a7f4 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Thu, 5 Oct 2023 06:19:47 +0900 Subject: [PATCH 33/49] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EB=8B=AC?= =?UTF-8?q?=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/comments/[slug]/route.ts | 72 +++++++++++++++++++++++++++++ app/api/comments/route.ts | 0 app/article/[slug]/page.tsx | 3 +- components/comments/CommentBox.tsx | 19 ++++++-- components/comments/CommentCard.tsx | 10 ++-- components/comments/CommentForm.tsx | 31 ++++++++++++- 6 files changed, 124 insertions(+), 11 deletions(-) create mode 100644 app/api/comments/[slug]/route.ts delete mode 100644 app/api/comments/route.ts diff --git a/app/api/comments/[slug]/route.ts b/app/api/comments/[slug]/route.ts new file mode 100644 index 00000000..ae975b0d --- /dev/null +++ b/app/api/comments/[slug]/route.ts @@ -0,0 +1,72 @@ +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}/comments`, { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Token ${token}`, + }, + }); + + console.log(res); + + return NextResponse.json({ message: 'Comment Get Success', success: true, data: res }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } +} + +async function POST(req: NextRequest, route: { params: { slug: string } }) { + try { + const body = await req.json(); + const slug = route.params.slug; + const token = req.cookies.get('token')?.value || ''; + console.log(body); + + console.log(slug); + console.log(token); + + const res = await http.post(`/articles/${slug}/comments`, body, { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Token ${token}`, + }, + }); + + console.log(res); + + return NextResponse.json({ message: 'Comment Create 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 || ''; + + console.log(slug); + console.log(token); + + const res = await http.delete(`/articles/${slug}/comments`, { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Token ${token}`, + }, + }); + + console.log(res); + + return NextResponse.json({ message: 'Comment Delete Success', success: true, data: res }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } +} + +export { GET, POST, DELETE }; diff --git a/app/api/comments/route.ts b/app/api/comments/route.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/app/article/[slug]/page.tsx b/app/article/[slug]/page.tsx index d2b5e878..2cb692c3 100644 --- a/app/article/[slug]/page.tsx +++ b/app/article/[slug]/page.tsx @@ -67,8 +67,7 @@ const ArticlePage = ({ params: { slug } }: Props) => {
- - +
); }; diff --git a/components/comments/CommentBox.tsx b/components/comments/CommentBox.tsx index cdf8010e..90541af9 100644 --- a/components/comments/CommentBox.tsx +++ b/components/comments/CommentBox.tsx @@ -6,15 +6,28 @@ import CommentForm from './CommentForm'; import CommentCard from './CommentCard'; import { flexCenter, flexRow, textCenter } from '@/styles/common.css'; import { User } from '@/types'; +import { useQuery } from '@tanstack/react-query'; -const CommentBox = () => { +const CommentBox = ({ slug }: { slug: string }) => { const { email } = useUserStore() as User; + const { data: comments } = useQuery({ + queryKey: ['comments', slug], + queryFn: async () => fetch(`/api/comments/${slug}`).then(res => res.json()), + select: res => res.data.comments, + }); + + console.log(comments); + return (
{email ? (
- - + +
+ {comments.map(comment => ( + + ))} +
) : (
diff --git a/components/comments/CommentCard.tsx b/components/comments/CommentCard.tsx index a0178ed9..2b2eb8b7 100644 --- a/components/comments/CommentCard.tsx +++ b/components/comments/CommentCard.tsx @@ -3,17 +3,19 @@ import { TrashIcon } from '@/composables/icons'; import { commentContent, commentFormFooter, commnetCard } from '@/styles/comments.css'; import { circle, flexCenter } from '@/styles/common.css'; +import { Comment } from '@/types'; +import { formatDate } from '@/utils'; import Image from 'next/image'; type Props = { - author: any; + comment: Comment; }; -const CommentCard = ({ author }: Props) => { +const CommentCard = ({ comment }: Props) => { const handleTrashClick = () => { console.log('쓰레기 클릭'); }; return (
-
ㅋㅋㅋ
+
{comment.body}
{ height={20} alt="iamge" /> -   hyeon9782 September 12, 2023 +   {comment.author.username} {formatDate(comment.createAt)}
diff --git a/components/comments/CommentForm.tsx b/components/comments/CommentForm.tsx index 177a27db..9651f6f5 100644 --- a/components/comments/CommentForm.tsx +++ b/components/comments/CommentForm.tsx @@ -2,15 +2,42 @@ import { commentForm, commentFormFooter, commentSubmitButton, commentTextarea } from '@/styles/comments.css'; import { circle, fillGreenButton } from '@/styles/common.css'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import Image from 'next/image'; +import { useState } from 'react'; -const CommentForm = () => { +const CommentForm = ({ slug }: { slug: string }) => { + const queryClient = useQueryClient(); + const [comment, setComment] = useState(''); + const { mutate } = useMutation({ + mutationFn: async (comment: string) => + fetch(`/api/comments/${slug}`, { method: 'POST', body: JSON.stringify({ comment: { body: comment } }) }).then( + res => res.json() + ), + onSuccess: () => { + alert('성공!'); + queryClient.invalidateQueries(['comments', slug]); + }, + onError: () => { + alert('실패'); + }, + }); const handleSubmit = (e: any) => { e.preventDefault(); + console.log(comment); + + mutate(e.target.comment.value); + setComment(''); }; return (
- +
Date: Thu, 5 Oct 2023 06:36:45 +0900 Subject: [PATCH 34/49] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/comments/[slug]/route.ts | 8 +++++++- components/comments/CommentBox.tsx | 2 +- components/comments/CommentCard.tsx | 22 +++++++++++++++++----- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/app/api/comments/[slug]/route.ts b/app/api/comments/[slug]/route.ts index ae975b0d..c7c9cb55 100644 --- a/app/api/comments/[slug]/route.ts +++ b/app/api/comments/[slug]/route.ts @@ -48,18 +48,24 @@ async function POST(req: NextRequest, route: { params: { slug: string } }) { async function DELETE(req: NextRequest, route: { params: { slug: string } }) { try { + const { searchParams } = new URL(req.url); + + const id = searchParams.get('id'); const slug = route.params.slug; + const token = req.cookies.get('token')?.value || ''; console.log(slug); console.log(token); + console.log('삭제 전'); - const res = await http.delete(`/articles/${slug}/comments`, { + const res = await http.delete(`/articles/${slug}/comments/${id}`, { headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: `Token ${token}`, }, }); + console.log('삭제 후'); console.log(res); diff --git a/components/comments/CommentBox.tsx b/components/comments/CommentBox.tsx index 90541af9..b629d448 100644 --- a/components/comments/CommentBox.tsx +++ b/components/comments/CommentBox.tsx @@ -25,7 +25,7 @@ const CommentBox = ({ slug }: { slug: string }) => {
{comments.map(comment => ( - + ))}
diff --git a/components/comments/CommentCard.tsx b/components/comments/CommentCard.tsx index 2b2eb8b7..4b7f3d21 100644 --- a/components/comments/CommentCard.tsx +++ b/components/comments/CommentCard.tsx @@ -1,17 +1,29 @@ 'use client'; import { TrashIcon } from '@/composables/icons'; +import useUserStore from '@/stores/useUserStore'; import { commentContent, commentFormFooter, commnetCard } from '@/styles/comments.css'; import { circle, flexCenter } from '@/styles/common.css'; -import { Comment } from '@/types'; +import { Comment, User } from '@/types'; import { formatDate } from '@/utils'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import Image from 'next/image'; type Props = { comment: Comment; + slug: string; }; -const CommentCard = ({ comment }: Props) => { - const handleTrashClick = () => { - console.log('쓰레기 클릭'); +const CommentCard = ({ comment, slug }: Props) => { + const queryClient = useQueryClient(); + const { username } = useUserStore() as User; + const { mutate } = useMutation({ + mutationFn: async (slug: string) => + fetch(`/api/comments/${slug}?id=${comment.id}`, { method: 'DELETE' }).then(res => res.json()), + onSuccess: () => { + queryClient.invalidateQueries(['comments', slug]); + }, + }); + const handleTrashClick = (slug: string) => { + mutate(slug); }; return (
@@ -27,7 +39,7 @@ const CommentCard = ({ comment }: Props) => { />   {comment.author.username} {formatDate(comment.createAt)}
- + {comment.author.username === username && handleTrashClick(slug)} />}
); From 2819603f20baf2f2e6d2f06b3b88ba4f78460a03 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Thu, 5 Oct 2023 07:01:04 +0900 Subject: [PATCH 35/49] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EC=A0=95?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/user/route.ts | 2 ++ app/settings/page.tsx | 10 +++++++++- stores/useUserStore.ts | 1 + types/index.ts | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/api/user/route.ts b/app/api/user/route.ts index 9e8cda77..3bcacb5c 100644 --- a/app/api/user/route.ts +++ b/app/api/user/route.ts @@ -16,6 +16,8 @@ export async function GET(req: NextRequest) { export async function PUT(req: NextRequest) { const { value } = req.cookies.get('token'); const user = await req.json(); + console.log('Route'); + console.log(user); return updateUserAPI(user, value); } diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 2f91e0d3..f8ab7048 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -12,7 +12,7 @@ import { useState } from 'react'; const SettingsPage = () => { const router = useRouter(); - const { logout, email, username, image, bio } = useUserStore() as User & UserAction; + const { logout, updateUser, email, username, image, bio } = useUserStore() as User & UserAction; // 초기화 함수로 전환 const [formData, setFormData] = useState({ @@ -28,6 +28,13 @@ const SettingsPage = () => { onSuccess: data => { console.log(data); console.log('성공'); + updateUser({ + ...formData, + }); + router.push(`/@${username}`); + }, + onError: () => { + alert('실패'); }, }); @@ -36,6 +43,7 @@ const SettingsPage = () => { mutate({ ...formData, + password: formData && formData.password, }); }; diff --git a/stores/useUserStore.ts b/stores/useUserStore.ts index b4f367c6..0414cd78 100644 --- a/stores/useUserStore.ts +++ b/stores/useUserStore.ts @@ -7,6 +7,7 @@ const initialState: User = { email: '', bio: '', image: '', + password: '', }; const useUserStore = create( diff --git a/types/index.ts b/types/index.ts index 94750ccc..bf24831e 100644 --- a/types/index.ts +++ b/types/index.ts @@ -78,6 +78,7 @@ export type User = { username: string; bio: string; image: string; + password?: string; }; export type UserAction = { From aa6dfaf744e7ce23ca8624400fa2c8be6c5741be Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Thu, 5 Oct 2023 16:25:05 +0900 Subject: [PATCH 36/49] =?UTF-8?q?feat:=20Follow=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/articles/[slug]/route.ts | 27 +++++++++++++++++++++++++-- app/article/[slug]/page.tsx | 7 ++++--- components/article/ArticleList.tsx | 4 ++++ components/user/FollowButton.tsx | 7 +++++-- hooks/useArticles.ts | 7 +++---- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/app/api/articles/[slug]/route.ts b/app/api/articles/[slug]/route.ts index 0b019bf2..e0a60bb4 100644 --- a/app/api/articles/[slug]/route.ts +++ b/app/api/articles/[slug]/route.ts @@ -1,13 +1,36 @@ import { http } from '@/utils/http'; import { NextRequest, NextResponse } from 'next/server'; -async function GET(req: NextRequest, route: { params: { slug: string } }) {} +async function GET(req: NextRequest, route: { params: { slug: string } }) { + try { + console.log('GET Route'); + + const slug = route.params.slug; + const token = req.cookies.get('token')?.value || ''; + console.log(slug); + console.log(token); + + const res = await http.get(`/articles/${slug}`, { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Token ${token}`, + }, + }); + + console.log(res); + + return NextResponse.json({ message: 'Article Get Success', success: true, data: res }); + } catch (error) { + 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 || ''; - console.log('좋아요 Route'); + console.log(slug); console.log(token); console.log(body); diff --git a/app/article/[slug]/page.tsx b/app/article/[slug]/page.tsx index 2cb692c3..bf390341 100644 --- a/app/article/[slug]/page.tsx +++ b/app/article/[slug]/page.tsx @@ -24,10 +24,11 @@ const ArticlePage = ({ params: { slug } }: Props) => { const { data: article } = useQuery({ queryKey: ['article', slug], - queryFn: async () => await getArticleAPI(slug), + queryFn: async () => await fetch(`/api/articles/${slug}`).then(res => res.json()), enabled: !!slug, - select: res => res.article, + select: res => res.data.article, }); + console.log('Detail'); console.log(article); @@ -48,7 +49,7 @@ const ArticlePage = ({ params: { slug } }: Props) => {
) : (
- +
)} diff --git a/components/article/ArticleList.tsx b/components/article/ArticleList.tsx index b064b808..da60ad64 100644 --- a/components/article/ArticleList.tsx +++ b/components/article/ArticleList.tsx @@ -16,6 +16,10 @@ const ArticleList = ({ username }: Props) => { const { tab } = useCurrentTab(); const { data } = useArticles(targetRef, tab, username); + console.log('List'); + + console.log(data); + return (
{data?.pages?.at(0)?.articles?.length === 0 ? ( diff --git a/components/user/FollowButton.tsx b/components/user/FollowButton.tsx index f33f92b7..a7c5d182 100644 --- a/components/user/FollowButton.tsx +++ b/components/user/FollowButton.tsx @@ -6,8 +6,9 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; type Props = { author: any; + slug: string; }; -const FollowButton = ({ author: { username, following } }: Props) => { +const FollowButton = ({ author: { username, following }, slug }: Props) => { const router = useRouter(); const queryClient = useQueryClient(); @@ -17,7 +18,9 @@ const FollowButton = ({ author: { username, following } }: Props) => { return fetch(`/api/profiles/${username}`, { method }).then(res => res.json()); }, onSuccess: data => { - queryClient.invalidateQueries(['articles', 'global']); + queryClient.invalidateQueries(['article', slug]); + console.log('Success'); + console.log(data); }, onError: error => { diff --git a/hooks/useArticles.ts b/hooks/useArticles.ts index 56ef07c9..433289f5 100644 --- a/hooks/useArticles.ts +++ b/hooks/useArticles.ts @@ -9,14 +9,13 @@ const useArticles = (ref: RefObject, tab: string, username = '') => queryFn: async ({ pageParam = 0 }) => { switch (tab) { case 'global': - // return await getArticlesAPI(pageParam); - return fetch(`/api/articles?page=${pageParam}`).then(res => res.json()); + return await fetch(`/api/articles?page=${pageParam}`).then(res => res.json()); case 'my': - return fetch(`/api/articles/my?username=${username}&page=${pageParam}`).then(res => res.json()); + return await fetch(`/api/articles/my?username=${username}&page=${pageParam}`).then(res => res.json()); case 'favorited': return await getArticlesWithFavoritedAPI(username); case 'your': - return fetch(`/api/articles/feed?page=${pageParam}`).then(res => res.json()); + return await fetch(`/api/articles/feed?page=${pageParam}`).then(res => res.json()); default: return await getArticlesWithTagAPI(tab, pageParam); } From b1539e12a1658928056f048f15fd4603136402e4 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Thu, 5 Oct 2023 17:07:48 +0900 Subject: [PATCH 37/49] =?UTF-8?q?feat:=20LoginForm=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20useLogin=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=ED=9B=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/article/[slug]/page.tsx | 8 +--- app/login/page.tsx | 76 ++------------------------------ components/account/LoginForm.tsx | 74 +++++++++++++++++++++++++++++++ hooks/useLogin.ts | 15 +++++++ 4 files changed, 94 insertions(+), 79 deletions(-) create mode 100644 components/account/LoginForm.tsx create mode 100644 hooks/useLogin.ts diff --git a/app/article/[slug]/page.tsx b/app/article/[slug]/page.tsx index bf390341..b7af5a5d 100644 --- a/app/article/[slug]/page.tsx +++ b/app/article/[slug]/page.tsx @@ -7,7 +7,6 @@ import TagList from '@/components/tags/TagList'; import FavoriteButton from '@/components/user/FavoriteButton'; import FollowButton from '@/components/user/FollowButton'; import UserBox from '@/components/user/UserBox'; -import { getArticleAPI } from '@/services/articles'; import useUserStore from '@/stores/useUserStore'; import { articleContent, articleDetailTitle } from '@/styles/article.css'; import { container, flex, justifyCenter, paddingTB } from '@/styles/common.css'; @@ -18,8 +17,6 @@ type Props = { params: { slug: string }; }; const ArticlePage = ({ params: { slug } }: Props) => { - console.log(slug); - const { username } = useUserStore() as User; const { data: article } = useQuery({ @@ -28,9 +25,6 @@ const ArticlePage = ({ params: { slug } }: Props) => { enabled: !!slug, select: res => res.data.article, }); - console.log('Detail'); - - console.log(article); const { title, author, createdAt, body, tagList, favoritesCount } = article; @@ -63,7 +57,7 @@ const ArticlePage = ({ params: { slug } }: Props) => {
-   +  
diff --git a/app/login/page.tsx b/app/login/page.tsx index ad9e84e9..70a62af3 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,81 +1,13 @@ -'use client'; - -import useUserStore from '@/stores/useUserStore'; -import { form, question, title } from '@/styles/account.css'; -import { input, container, flexCenter, flexRow, fillGreenButton } from '@/styles/common.css'; -import { buttonBox } from '@/styles/layout.css'; -import { LoginUser, UserAction } from '@/types'; -import { useMutation } from '@tanstack/react-query'; -import { useRouter } from 'next/navigation'; -import { ChangeEvent, FormEvent, useState } from 'react'; +import LoginForm from '@/components/account/LoginForm'; +import { question, title } from '@/styles/account.css'; +import { container, flexCenter, flexRow } from '@/styles/common.css'; const LoginPage = () => { - const router = useRouter(); - const { login } = useUserStore() as UserAction; - - const [formData, setFormData] = useState({ - email: '', - password: '', - }); - - const { mutate, isLoading } = useMutation({ - mutationFn: (formData: any) => - fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ ...formData }) }).then(res => res.json()), - onError: error => { - setFormData({ - email: '', - password: '', - }); - alert('아이디 또는 비밀번호가 잘못되었습니다.'); - }, - onSuccess: res => { - console.log(res); - login({ ...res.user }); - router.push('/'); - }, - }); - - const handleSubmit = (e: FormEvent) => { - e.preventDefault(); - mutate({ - ...formData, - }); - }; - - const handleChange = (e: ChangeEvent) => { - const { name, value } = e.target; - setFormData(prev => ({ - ...prev, - [name]: value, - })); - }; return (
Sign in
Need an account?
- - - -
- -
- +
); }; diff --git a/components/account/LoginForm.tsx b/components/account/LoginForm.tsx new file mode 100644 index 00000000..0dfcdbb6 --- /dev/null +++ b/components/account/LoginForm.tsx @@ -0,0 +1,74 @@ +'use client'; +import useLogin from '@/hooks/useLogin'; +import useUserStore from '@/stores/useUserStore'; +import { form } from '@/styles/account.css'; +import { fillGreenButton, input } from '@/styles/common.css'; +import { buttonBox } from '@/styles/layout.css'; +import { LoginUser, UserAction } from '@/types'; +import { useRouter } from 'next/navigation'; +import { ChangeEvent, FormEvent, useState } from 'react'; + +const LoginForm = () => { + const router = useRouter(); + const { login } = useUserStore() as UserAction; + + const [loginUser, setLoginUser] = useState({ + email: '', + password: '', + }); + + const onSuccess = (res: any) => { + login({ ...res.user }); + router.push('/'); + }; + + const onError = (err: any) => { + console.log(err.message); + alert('이메일 또는 비밀번호가 잘못되었습니다.'); + }; + + const { mutate, isLoading } = useLogin({ onSuccess, onError }); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + mutate({ + ...loginUser, + }); + }; + + const handleChange = (e: ChangeEvent) => { + const { name, value } = e.target; + setLoginUser(prev => ({ + ...prev, + [name]: value, + })); + }; + + return ( +
+ + +
+ +
+
+ ); +}; + +export default LoginForm; diff --git a/hooks/useLogin.ts b/hooks/useLogin.ts new file mode 100644 index 00000000..a210a5ea --- /dev/null +++ b/hooks/useLogin.ts @@ -0,0 +1,15 @@ +import { LoginUser } from '@/types'; +import { useMutation } from '@tanstack/react-query'; + +const useLogin = ({ onSuccess, onError }: { onSuccess: (res: any) => void; onError: (err: any) => void }) => { + return useMutation({ + mutationFn: async (loginUser: LoginUser) => + await fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ ...loginUser }) }).then(res => + res.json() + ), + onError, + onSuccess, + }); +}; + +export default useLogin; From 518e4660e5fcb715a1eb59f7a8f8a66a85424473 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Thu, 5 Oct 2023 17:53:28 +0900 Subject: [PATCH 38/49] =?UTF-8?q?feat=20&=20refactor:=20Login,=20Register?= =?UTF-8?q?=20Page=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20?= =?UTF-8?q?=20useAuth=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/login/page.tsx | 9 +- app/register/page.tsx | 89 ++----------------- components/account/FormHead.tsx | 12 +++ .../account/{LoginForm.tsx => SignInForm.tsx} | 30 +++---- components/account/SignUpForm.tsx | 75 ++++++++++++++++ hooks/useAuth.ts | 48 ++++++++++ hooks/useLogin.ts | 15 ---- 7 files changed, 159 insertions(+), 119 deletions(-) create mode 100644 components/account/FormHead.tsx rename components/account/{LoginForm.tsx => SignInForm.tsx} (74%) create mode 100644 components/account/SignUpForm.tsx create mode 100644 hooks/useAuth.ts delete mode 100644 hooks/useLogin.ts diff --git a/app/login/page.tsx b/app/login/page.tsx index 70a62af3..54661b13 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,13 +1,12 @@ -import LoginForm from '@/components/account/LoginForm'; -import { question, title } from '@/styles/account.css'; +import FormHead from '@/components/account/FormHead'; +import SignInForm from '@/components/account/SignInForm'; import { container, flexCenter, flexRow } from '@/styles/common.css'; const LoginPage = () => { return (
-
Sign in
-
Need an account?
- + +
); }; diff --git a/app/register/page.tsx b/app/register/page.tsx index f060dae9..9bb7eb6f 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -1,91 +1,12 @@ -'use client'; -import { registerUserAPI } from '@/services/users'; -import useUserStore from '@/stores/useUserStore'; -import { form, question, title } from '@/styles/account.css'; -import { input, container, flexRow, flexCenter, fillGreenButton } from '@/styles/common.css'; -import { buttonBox } from '@/styles/layout.css'; -import { NewUser, UserAction } from '@/types'; -import { useMutation } from '@tanstack/react-query'; -import { useRouter } from 'next/navigation'; - -import { ChangeEvent, FormEvent, useState } from 'react'; +import FormHead from '@/components/account/FormHead'; +import SignUpForm from '@/components/account/SignUpForm'; +import { container, flexRow, flexCenter } from '@/styles/common.css'; const RegisterPage = () => { - const router = useRouter(); - const { login } = useUserStore() as UserAction; - const [formData, setFormData] = useState({ - username: '', - email: '', - password: '', - }); - - const { mutate, isLoading } = useMutation({ - mutationFn: registerUserAPI, - onError: error => { - alert('회원가입에 실패했습니다.'); - console.log(error); - }, - onSuccess: res => { - alert('회원가입에 성공했습니다.'); - login({ - ...res.user, - }); - router.push('/'); - }, - }); - - const handleSubmit = (e: FormEvent) => { - e.preventDefault(); - - mutate({ - ...formData, - }); - }; - - const handleChange = (e: ChangeEvent) => { - const { name, value } = e.target; - setFormData(prev => ({ - ...prev, - [name]: value, - })); - }; - return (
-
Sign up
-
Have an account?
-
- - - -
- -
-
+ +
); }; diff --git a/components/account/FormHead.tsx b/components/account/FormHead.tsx new file mode 100644 index 00000000..d85e4079 --- /dev/null +++ b/components/account/FormHead.tsx @@ -0,0 +1,12 @@ +import { question, title } from '@/styles/account.css'; + +const FormHead = ({ titleText, questionText }: { titleText: string; questionText: string }) => { + return ( + <> +
{titleText}
+
{questionText}
+ + ); +}; + +export default FormHead; diff --git a/components/account/LoginForm.tsx b/components/account/SignInForm.tsx similarity index 74% rename from components/account/LoginForm.tsx rename to components/account/SignInForm.tsx index 0dfcdbb6..9330e1d1 100644 --- a/components/account/LoginForm.tsx +++ b/components/account/SignInForm.tsx @@ -1,5 +1,5 @@ 'use client'; -import useLogin from '@/hooks/useLogin'; +import useAuth from '@/hooks/useAuth'; import useUserStore from '@/stores/useUserStore'; import { form } from '@/styles/account.css'; import { fillGreenButton, input } from '@/styles/common.css'; @@ -8,30 +8,30 @@ import { LoginUser, UserAction } from '@/types'; import { useRouter } from 'next/navigation'; import { ChangeEvent, FormEvent, useState } from 'react'; -const LoginForm = () => { +const SignInForm = () => { const router = useRouter(); - const { login } = useUserStore() as UserAction; + // const { login } = useUserStore() as UserAction; const [loginUser, setLoginUser] = useState({ email: '', password: '', }); - const onSuccess = (res: any) => { - login({ ...res.user }); - router.push('/'); - }; + const { login } = useAuth(); - const onError = (err: any) => { - console.log(err.message); - alert('이메일 또는 비밀번호가 잘못되었습니다.'); - }; + // const onSuccess = (res: any) => { + // login({ ...res.user }); + // router.push('/'); + // }; - const { mutate, isLoading } = useLogin({ onSuccess, onError }); + // const onError = (err: any) => { + // console.log(err.message); + // alert('이메일 또는 비밀번호가 잘못되었습니다.'); + // }; const handleSubmit = (e: FormEvent) => { e.preventDefault(); - mutate({ + login({ ...loginUser, }); }; @@ -65,10 +65,10 @@ const LoginForm = () => { required />
- +
); }; -export default LoginForm; +export default SignInForm; diff --git a/components/account/SignUpForm.tsx b/components/account/SignUpForm.tsx new file mode 100644 index 00000000..01427286 --- /dev/null +++ b/components/account/SignUpForm.tsx @@ -0,0 +1,75 @@ +'use client'; + +import useAuth from '@/hooks/useAuth'; +import useUserStore from '@/stores/useUserStore'; +import { form } from '@/styles/account.css'; +import { fillGreenButton, input } from '@/styles/common.css'; +import { buttonBox } from '@/styles/layout.css'; +import { NewUser, UserAction } from '@/types'; +import { useRouter } from 'next/navigation'; +import { ChangeEvent, FormEvent, useState } from 'react'; + +const SignUpForm = () => { + const router = useRouter(); + const { login } = useUserStore() as UserAction; + const [newUser, setNewUser] = useState({ + username: '', + email: '', + password: '', + }); + + const { signup } = useAuth(); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + + signup({ + ...newUser, + }); + }; + + const handleChange = (e: ChangeEvent) => { + const { name, value } = e.target; + setNewUser(prev => ({ + ...prev, + [name]: value, + })); + }; + + return ( +
+ + + +
+ +
+
+ ); +}; + +export default SignUpForm; diff --git a/hooks/useAuth.ts b/hooks/useAuth.ts new file mode 100644 index 00000000..14c18812 --- /dev/null +++ b/hooks/useAuth.ts @@ -0,0 +1,48 @@ +import { LoginUser, NewUser } from '@/types'; +import { useMutation } from '@tanstack/react-query'; +// 로그인 로그아웃 회원가입 로그인 확인 +const useAuth = () => { + // 로그인 + const { mutate: login } = useMutation({ + mutationFn: async (loginUser: LoginUser) => + await fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ ...loginUser }) }).then(res => + res.json() + ), + onSuccess: () => { + alert('로그인 성공!'); + }, + onError: (err: any) => { + console.log(err); + alert('에러 발생'); + }, + }); + + // 회원가입 + const { mutate: signup } = useMutation({ + mutationFn: async (newUser: NewUser) => + await fetch('/api/auth/signup', { method: 'POST', body: JSON.stringify({ ...newUser }) }).then(res => res.json()), + onSuccess: () => { + alert('회원가입 성공!'); + }, + onError: (err: any) => { + console.log(err); + alert('에러 발생'); + }, + }); + + // 로그아웃 + const { mutate: signOut } = useMutation({ + mutationFn: () => fetch('/api/auth/logout').then(res => res.json()), + onSuccess: () => { + alert('로그아웃 성공!'); + }, + onError: (err: any) => { + console.log(err); + alert('에러 발생'); + }, + }); + + return { login, signup, signOut }; +}; + +export default useAuth; diff --git a/hooks/useLogin.ts b/hooks/useLogin.ts deleted file mode 100644 index a210a5ea..00000000 --- a/hooks/useLogin.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { LoginUser } from '@/types'; -import { useMutation } from '@tanstack/react-query'; - -const useLogin = ({ onSuccess, onError }: { onSuccess: (res: any) => void; onError: (err: any) => void }) => { - return useMutation({ - mutationFn: async (loginUser: LoginUser) => - await fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ ...loginUser }) }).then(res => - res.json() - ), - onError, - onSuccess, - }); -}; - -export default useLogin; From 3f5c018588566ef92f4f780f592388b37aedfd3d Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Sun, 8 Oct 2023 00:18:26 +0900 Subject: [PATCH 39/49] =?UTF-8?q?refactor=20&=20feat:=20SettingForm=20,=20?= =?UTF-8?q?LogoutButton=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[username]/page.tsx | 2 +- app/settings/page.tsx | 126 ++------------------------- components/account/SignInForm.tsx | 20 ++--- components/settings/LogoutButton.tsx | 29 ++++++ components/settings/SettingForm.tsx | 108 +++++++++++++++++++++++ hooks/useAuth.ts | 45 +++++----- stores/useUserStore.ts | 2 +- types/index.ts | 2 +- 8 files changed, 179 insertions(+), 155 deletions(-) create mode 100644 components/settings/LogoutButton.tsx create mode 100644 components/settings/SettingForm.tsx diff --git a/app/[username]/page.tsx b/app/[username]/page.tsx index ae31d464..2a7e7702 100644 --- a/app/[username]/page.tsx +++ b/app/[username]/page.tsx @@ -21,7 +21,7 @@ const ProfilePage = ({ params: { username } }: Props) => { const { data: profile } = useQuery({ queryKey: ['profile', username], queryFn: () => fetch(`/api/profiles/${username}`).then(res => res.json()), - enabled: !!username, + // enabled: !!username, select: res => res.response.profile, }); diff --git a/app/settings/page.tsx b/app/settings/page.tsx index f8ab7048..c7a1b13c 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,129 +1,17 @@ -'use client'; -import useUserStore from '@/stores/useUserStore'; -import { articleTextarea } from '@/styles/article.css'; -import { container, flex, hr, input } from '@/styles/common.css'; -import { logoutButton, settingBlock, settingForm, settingTitle, updateButton } from '@/styles/settings.css'; -import { User, UserAction } from '@/types'; -import { useMutation } from '@tanstack/react-query'; -import { useRouter } from 'next/navigation'; +import LogoutButton from '@/components/settings/LogoutButton'; +import SettingForm from '@/components/settings/SettingForm'; -import { useState } from 'react'; +import { container } from '@/styles/common.css'; +import { settingBlock, settingTitle } from '@/styles/settings.css'; const SettingsPage = () => { - const router = useRouter(); - - const { logout, updateUser, email, username, image, bio } = useUserStore() as User & UserAction; - - // 초기화 함수로 전환 - const [formData, setFormData] = useState({ - image, - username, - bio: bio ? bio : '', - email, - password: '', - }); - const { mutate, isLoading } = useMutation({ - mutationFn: (formData: any) => - fetch('/api/auth/user', { method: 'PUT', body: JSON.stringify(formData) }).then(res => res.json()), - onSuccess: data => { - console.log(data); - console.log('성공'); - updateUser({ - ...formData, - }); - router.push(`/@${username}`); - }, - onError: () => { - alert('실패'); - }, - }); - - const handleSubmit = async (e: any) => { - e.preventDefault(); - - mutate({ - ...formData, - password: formData && formData.password, - }); - }; - - const handleChange = (e: any) => { - setFormData(prev => ({ - ...prev, - [e.target.name]: e.target.value, - })); - }; - - const signOut = async () => { - try { - await fetch('/api/auth/logout'); - - logout(); - router.push('/login'); - } catch (error: any) { - console.error(error.message); - } - }; return (
Your Settings
-
- - - - - -
- -
-
-
- -
- + +
+
); diff --git a/components/account/SignInForm.tsx b/components/account/SignInForm.tsx index 9330e1d1..de88fb02 100644 --- a/components/account/SignInForm.tsx +++ b/components/account/SignInForm.tsx @@ -10,24 +10,24 @@ import { ChangeEvent, FormEvent, useState } from 'react'; const SignInForm = () => { const router = useRouter(); - // const { login } = useUserStore() as UserAction; + const { saveUserInfo } = useUserStore() as UserAction; const [loginUser, setLoginUser] = useState({ email: '', password: '', }); - const { login } = useAuth(); + const loginSuccess = (res: any) => { + saveUserInfo({ ...res.user }); + router.push('/'); + }; - // const onSuccess = (res: any) => { - // login({ ...res.user }); - // router.push('/'); - // }; + const loginError = (err: any) => { + console.log(err.message); + alert('이메일 또는 비밀번호가 잘못되었습니다.'); + }; - // const onError = (err: any) => { - // console.log(err.message); - // alert('이메일 또는 비밀번호가 잘못되었습니다.'); - // }; + const { login } = useAuth({ loginSuccess, loginError }); const handleSubmit = (e: FormEvent) => { e.preventDefault(); diff --git a/components/settings/LogoutButton.tsx b/components/settings/LogoutButton.tsx new file mode 100644 index 00000000..a429704c --- /dev/null +++ b/components/settings/LogoutButton.tsx @@ -0,0 +1,29 @@ +'use client'; +import useAuth from '@/hooks/useAuth'; +import useUserStore from '@/stores/useUserStore'; +import { flex } from '@/styles/common.css'; +import { logoutButton } from '@/styles/settings.css'; +import { UserAction } from '@/types'; + +const LogoutButton = () => { + const { logout } = useUserStore() as UserAction; + const signoutSuccess = res => { + logout(); + router.push('/login'); + }; + + const signoutError = err => { + console.error(err.message); + }; + + const { signOut } = useAuth({ signoutSuccess, signoutError }); + return ( +
+ +
+ ); +}; + +export default LogoutButton; diff --git a/components/settings/SettingForm.tsx b/components/settings/SettingForm.tsx new file mode 100644 index 00000000..a28cc249 --- /dev/null +++ b/components/settings/SettingForm.tsx @@ -0,0 +1,108 @@ +'use client'; +import useUserStore from '@/stores/useUserStore'; +import { articleTextarea } from '@/styles/article.css'; +import { input } from '@/styles/common.css'; +import { settingForm, updateButton } from '@/styles/settings.css'; +import { User, UserAction } from '@/types'; +import { useMutation } from '@tanstack/react-query'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +const SettingForm = () => { + const router = useRouter(); + + const { updateUser, email, username, image, bio } = useUserStore() as User & UserAction; + + // 초기화 함수로 전환 + const [formData, setFormData] = useState({ + image, + username, + bio: bio ? bio : '', + email, + password: '', + }); + const { mutate, isLoading } = useMutation({ + mutationFn: (formData: any) => + fetch('/api/auth/user', { method: 'PUT', body: JSON.stringify(formData) }).then(res => res.json()), + onSuccess: data => { + console.log(data); + console.log('성공'); + updateUser({ + ...formData, + }); + router.push(`/@${username}`); + }, + onError: () => { + alert('실패'); + }, + }); + const handleSubmit = async (e: any) => { + e.preventDefault(); + + mutate({ + ...formData, + password: formData && formData.password, + }); + }; + + const handleChange = (e: any) => { + setFormData(prev => ({ + ...prev, + [e.target.name]: e.target.value, + })); + }; + return ( +
+ + + + + +
+ +
+
+ ); +}; + +export default SettingForm; diff --git a/hooks/useAuth.ts b/hooks/useAuth.ts index 14c18812..4496a32f 100644 --- a/hooks/useAuth.ts +++ b/hooks/useAuth.ts @@ -1,45 +1,44 @@ import { LoginUser, NewUser } from '@/types'; import { useMutation } from '@tanstack/react-query'; -// 로그인 로그아웃 회원가입 로그인 확인 -const useAuth = () => { +// 로그인 / 로그아웃 / 회원가입 / 로그인 확인 +const useAuth = ({ + loginSuccess, + loginError, + signupSuccess, + signupError, + signoutSuccess, + signoutError, +}: { + loginSuccess?: (res: any) => void; + loginError?: (err: any) => void; + signupSuccess?: (res: any) => void; + signupError?: (err: any) => void; + signoutSuccess?: (res: any) => void; + signoutError?: (err: any) => void; +}) => { // 로그인 const { mutate: login } = useMutation({ mutationFn: async (loginUser: LoginUser) => await fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ ...loginUser }) }).then(res => res.json() ), - onSuccess: () => { - alert('로그인 성공!'); - }, - onError: (err: any) => { - console.log(err); - alert('에러 발생'); - }, + onSuccess: loginSuccess, + onError: loginError, }); // 회원가입 const { mutate: signup } = useMutation({ mutationFn: async (newUser: NewUser) => await fetch('/api/auth/signup', { method: 'POST', body: JSON.stringify({ ...newUser }) }).then(res => res.json()), - onSuccess: () => { - alert('회원가입 성공!'); - }, - onError: (err: any) => { - console.log(err); - alert('에러 발생'); - }, + onSuccess: signupSuccess, + onError: signupError, }); // 로그아웃 const { mutate: signOut } = useMutation({ mutationFn: () => fetch('/api/auth/logout').then(res => res.json()), - onSuccess: () => { - alert('로그아웃 성공!'); - }, - onError: (err: any) => { - console.log(err); - alert('에러 발생'); - }, + onSuccess: signoutSuccess, + onError: signoutError, }); return { login, signup, signOut }; diff --git a/stores/useUserStore.ts b/stores/useUserStore.ts index 0414cd78..64e543ba 100644 --- a/stores/useUserStore.ts +++ b/stores/useUserStore.ts @@ -14,7 +14,7 @@ const useUserStore = create( persist( set => ({ ...initialState, - login: user => { + saveUserInfo: user => { set(() => { const { email, username, bio, image } = user; diff --git a/types/index.ts b/types/index.ts index bf24831e..2bf404fe 100644 --- a/types/index.ts +++ b/types/index.ts @@ -82,7 +82,7 @@ export type User = { }; export type UserAction = { - login?: (user: User) => void; + saveUserInfo?: (user: User) => void; updateUser?: (user: User) => void; logout?: () => void; reset?: () => void; From e07785535d57a51eaf2d0b289d93c5b5b80c997a Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Sun, 8 Oct 2023 00:51:58 +0900 Subject: [PATCH 40/49] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B8=B0=EB=8A=A5=20route=20handler=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/auth/login/route.ts | 6 +++--- app/api/auth/signup/route.ts | 13 ++++++++++++- app/settings/page.tsx | 4 ++-- components/account/SignUpForm.tsx | 24 ++++++++++++++++++++++-- components/settings/LogoutButton.tsx | 6 ++++-- styles/settings.css.ts | 2 +- 6 files changed, 44 insertions(+), 11 deletions(-) diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 0c44f4fb..3cf4120d 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -5,15 +5,15 @@ export async function POST(request: NextRequest) { try { const body = await request.json(); - const req = await loginAPI(body); + const res = await loginAPI(body); const response = NextResponse.json({ message: 'Login successfull', success: true, - user: req.user, + user: res.user, }); - response.cookies.set('token', req.user.token, { + response.cookies.set('token', res.user.token, { httpOnly: true, path: '/', }); diff --git a/app/api/auth/signup/route.ts b/app/api/auth/signup/route.ts index 4b665b96..b51a384e 100644 --- a/app/api/auth/signup/route.ts +++ b/app/api/auth/signup/route.ts @@ -5,7 +5,18 @@ export async function POST(request: NextRequest) { try { const body = await request.json(); const res = await registerUserAPI(body); - return NextResponse.json({ message: 'User created successfully', success: true, res }); + + const response = NextResponse.json({ + message: 'Login successfull', + success: true, + user: res.user, + }); + + response.cookies.set('token', res.user.token, { + httpOnly: true, + path: '/', + }); + return response; } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 500 }); } diff --git a/app/settings/page.tsx b/app/settings/page.tsx index c7a1b13c..a1cbed6b 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,7 +1,7 @@ import LogoutButton from '@/components/settings/LogoutButton'; import SettingForm from '@/components/settings/SettingForm'; -import { container } from '@/styles/common.css'; +import { container, hr } from '@/styles/common.css'; import { settingBlock, settingTitle } from '@/styles/settings.css'; const SettingsPage = () => { @@ -10,7 +10,7 @@ const SettingsPage = () => {
Your Settings
-
+
diff --git a/components/account/SignUpForm.tsx b/components/account/SignUpForm.tsx index 01427286..5efc6074 100644 --- a/components/account/SignUpForm.tsx +++ b/components/account/SignUpForm.tsx @@ -11,14 +11,34 @@ import { ChangeEvent, FormEvent, useState } from 'react'; const SignUpForm = () => { const router = useRouter(); - const { login } = useUserStore() as UserAction; + const { saveUserInfo } = useUserStore() as UserAction; const [newUser, setNewUser] = useState({ username: '', email: '', password: '', }); - const { signup } = useAuth(); + const signupSuccess = (res: any) => { + console.log('Client'); + + console.log(res); + + saveUserInfo({ + ...res.user, + }); + router.push('/'); + }; + + const signupError = () => { + alert('회원가입에 실패했습니다.'); + setNewUser({ + username: '', + email: '', + password: '', + }); + }; + + const { signup } = useAuth({ signupSuccess, signupError }); const handleSubmit = (e: FormEvent) => { e.preventDefault(); diff --git a/components/settings/LogoutButton.tsx b/components/settings/LogoutButton.tsx index a429704c..404c0439 100644 --- a/components/settings/LogoutButton.tsx +++ b/components/settings/LogoutButton.tsx @@ -4,15 +4,17 @@ import useUserStore from '@/stores/useUserStore'; import { flex } from '@/styles/common.css'; import { logoutButton } from '@/styles/settings.css'; import { UserAction } from '@/types'; +import { useRouter } from 'next/navigation'; const LogoutButton = () => { + const router = useRouter(); const { logout } = useUserStore() as UserAction; - const signoutSuccess = res => { + const signoutSuccess = (res: any) => { logout(); router.push('/login'); }; - const signoutError = err => { + const signoutError = (err: any) => { console.error(err.message); }; diff --git a/styles/settings.css.ts b/styles/settings.css.ts index 85572eda..e98f148c 100644 --- a/styles/settings.css.ts +++ b/styles/settings.css.ts @@ -1,5 +1,5 @@ import { style } from '@vanilla-extract/css'; -import { button, fillGreenButton, redButton } from './common.css'; +import { fillGreenButton, redButton } from './common.css'; export const settingBlock = style({ display: 'flex', From 35edc4fb6f27ca3a893fcd900c803665f547655f Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Sun, 8 Oct 2023 01:17:16 +0900 Subject: [PATCH 41/49] =?UTF-8?q?feat=20&=20refactor:=20useProfile=20?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[username]/page.tsx | 10 +++------ components/user/FollowButton.tsx | 38 ++++++++++++++++++-------------- hooks/useProfile.ts | 38 ++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 24 deletions(-) create mode 100644 hooks/useProfile.ts diff --git a/app/[username]/page.tsx b/app/[username]/page.tsx index 2a7e7702..da9ca917 100644 --- a/app/[username]/page.tsx +++ b/app/[username]/page.tsx @@ -3,11 +3,11 @@ import ArticleList from '@/components/article/ArticleList'; import FollowButton from '@/components/user/FollowButton'; import { SettingIcon } from '@/composables/icons'; +import useProfile from '@/hooks/useProfile'; import useUserStore from '@/stores/useUserStore'; import { container } from '@/styles/common.css'; import { settingButton, userBlock, userImage, userInfo, userName } from '@/styles/profile.css'; import { User } from '@/types'; -import { useQuery } from '@tanstack/react-query'; import dynamic from 'next/dynamic'; import Image from 'next/image'; import Link from 'next/link'; @@ -18,12 +18,8 @@ type Props = { }; const ProfilePage = ({ params: { username } }: Props) => { const { username: currentUsername } = useUserStore() as User; - const { data: profile } = useQuery({ - queryKey: ['profile', username], - queryFn: () => fetch(`/api/profiles/${username}`).then(res => res.json()), - // enabled: !!username, - select: res => res.response.profile, - }); + + const { profile } = useProfile({ username }); console.log(profile); diff --git a/components/user/FollowButton.tsx b/components/user/FollowButton.tsx index a7c5d182..c3f6184b 100644 --- a/components/user/FollowButton.tsx +++ b/components/user/FollowButton.tsx @@ -1,8 +1,9 @@ 'use client'; import Button from '@/composables/Button'; import { PlusIcon } from '@/composables/icons'; +import useProfile from '@/hooks/useProfile'; import { fontSize } from '@/styles/common.css'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; type Props = { author: any; @@ -12,25 +13,28 @@ const FollowButton = ({ author: { username, following }, slug }: Props) => { const router = useRouter(); const queryClient = useQueryClient(); - const { mutate } = useMutation({ - mutationFn: async () => { - const method = following ? 'DELETE' : 'POST'; - return fetch(`/api/profiles/${username}`, { method }).then(res => res.json()); - }, - onSuccess: data => { - queryClient.invalidateQueries(['article', slug]); - console.log('Success'); + const onSuccess = (res: any) => { + console.log(res); + queryClient.invalidateQueries(['article', slug]); + }; - console.log(data); - }, - onError: error => { - console.error(error); - router.push('/login'); - }, - }); + const onError = (err: any) => { + console.error(err); + router.push('/login'); + }; + + const { follow, unFollow } = useProfile({ onSuccess, onError }); + + const handleButtonClick = () => { + if (following) { + unFollow(); + } else { + follow(); + } + }; return ( - ); diff --git a/hooks/useProfile.ts b/hooks/useProfile.ts new file mode 100644 index 00000000..2ba375d2 --- /dev/null +++ b/hooks/useProfile.ts @@ -0,0 +1,38 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; + +const useProfile = ({ + username, + onSuccess, + onError, +}: { + username?: string; + onSuccess: (res: any) => void; + onError: (err: any) => void; +}) => { + const { data: profile } = useQuery({ + queryKey: ['profile', username], + queryFn: () => fetch(`/api/profiles/${username}`).then(res => res.json()), + enabled: !!username, + select: res => res.response.profile, + }); + + const { mutate: follow } = useMutation({ + mutationFn: async () => { + return fetch(`/api/profiles/${username}`, { method: 'POST' }).then(res => res.json()); + }, + onSuccess, + onError, + }); + + const { mutate: unFollow } = useMutation({ + mutationFn: async () => { + return fetch(`/api/profiles/${username}`, { method: 'DELETE' }).then(res => res.json()); + }, + onSuccess, + onError, + }); + + return { profile, follow, unFollow }; +}; + +export default useProfile; From 2a38d1201629789d3d7565560d124fe1fc822593 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Sun, 8 Oct 2023 02:36:02 +0900 Subject: [PATCH 42/49] =?UTF-8?q?refactor:=20favorite,=20unfavorite=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20useArticles=EC=97=90=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/articles/[slug]/route.ts | 21 ++-------- app/api/articles/favorite/[slug]/route.ts | 14 +++---- app/api/articles/my/route.ts | 4 -- app/api/articles/new/route.ts | 7 ---- components/article/ArticleList.tsx | 13 ++----- components/article/ArticlePreview.tsx | 47 +++++++++++------------ hooks/useArticles.ts | 46 +++++++++++++++------- hooks/useInfiniteScroll.ts | 34 ---------------- hooks/useIntersectionObserver.ts | 11 ++++-- hooks/useProfile.ts | 2 +- services/articles.ts | 4 -- 11 files changed, 74 insertions(+), 129 deletions(-) delete mode 100644 hooks/useInfiniteScroll.ts diff --git a/app/api/articles/[slug]/route.ts b/app/api/articles/[slug]/route.ts index e0a60bb4..e6cd446e 100644 --- a/app/api/articles/[slug]/route.ts +++ b/app/api/articles/[slug]/route.ts @@ -3,12 +3,8 @@ import { NextRequest, NextResponse } from 'next/server'; async function GET(req: NextRequest, route: { params: { slug: string } }) { try { - console.log('GET Route'); - const slug = route.params.slug; const token = req.cookies.get('token')?.value || ''; - console.log(slug); - console.log(token); const res = await http.get(`/articles/${slug}`, { headers: { @@ -17,24 +13,19 @@ async function GET(req: NextRequest, route: { params: { slug: string } }) { }, }); - console.log(res); - return NextResponse.json({ message: 'Article Get Success', success: true, data: res }); - } catch (error) { + } 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 || ''; - console.log(slug); - console.log(token); - console.log(body); - const res = await http.put(`/articles/${slug}`, body, { headers: { 'Content-Type': 'application/json; charset=utf-8', @@ -42,20 +33,16 @@ async function PUT(req: NextRequest, route: { params: { slug: string } }) { }, }); - console.log(res); - 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 || ''; - console.log('좋아요 Route'); - console.log(slug); - console.log(token); const res = await http.delete(`/articles/${slug}`, { headers: { @@ -64,8 +51,6 @@ async function DELETE(req: NextRequest, route: { params: { slug: string } }) { }, }); - console.log(res); - return NextResponse.json({ message: 'Article Delete Success', success: true, data: res }); } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 400 }); diff --git a/app/api/articles/favorite/[slug]/route.ts b/app/api/articles/favorite/[slug]/route.ts index 1d11edf2..337dda24 100644 --- a/app/api/articles/favorite/[slug]/route.ts +++ b/app/api/articles/favorite/[slug]/route.ts @@ -2,13 +2,10 @@ import { http } from '@/utils/http'; import { NextRequest, NextResponse } from 'next/server'; // 좋아요 -export async function POST(request: NextRequest, route: { params: { slug: string } }) { +async function POST(request: NextRequest, route: { params: { slug: string } }) { try { const slug = route.params.slug; const token = request.cookies.get('token')?.value || ''; - console.log('좋아요 Route'); - console.log(slug); - console.log(token); const res = await http.post(`/articles/${slug}/favorite`, '', { headers: { @@ -17,8 +14,6 @@ export async function POST(request: NextRequest, route: { params: { slug: string }, }); - console.log(res); - return NextResponse.json({ message: 'Favorite Success', success: true, data: res }); } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 400 }); @@ -26,9 +21,8 @@ export async function POST(request: NextRequest, route: { params: { slug: string } // 좋아요 취소 -export async function DELETE(request: NextRequest, route: { params: { slug: string } }) { +async function DELETE(request: NextRequest, route: { params: { slug: string } }) { try { - console.log('좋아요 취소'); const slug = route.params.slug; const token = request.cookies.get('token')?.value || ''; const res = await http.delete(`/articles/${slug}/favorite`, { @@ -37,9 +31,11 @@ export async function DELETE(request: NextRequest, route: { params: { slug: stri Authorization: `Token ${token}`, }, }); - console.log(res); + return NextResponse.json({ message: 'Un Favorite Success', success: true, data: res }); } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 400 }); } } + +export { POST, DELETE }; diff --git a/app/api/articles/my/route.ts b/app/api/articles/my/route.ts index 1f8f9e58..be7aff7e 100644 --- a/app/api/articles/my/route.ts +++ b/app/api/articles/my/route.ts @@ -4,15 +4,11 @@ import { NextRequest, NextResponse } from 'next/server'; export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); - // const page = searchParams.get('page'); const username = searchParams.get('username') || ''; - console.log('username: ' + username); const token = request.cookies.get('token')?.value || ''; const { articles, articlesCount } = await getArticlesWithAuthorAPI(username, token); - console.log(articles); - return NextResponse.json({ articles, articlesCount }); } diff --git a/app/api/articles/new/route.ts b/app/api/articles/new/route.ts index aeadf2c3..e28ce3a4 100644 --- a/app/api/articles/new/route.ts +++ b/app/api/articles/new/route.ts @@ -6,11 +6,6 @@ export async function POST(request: NextRequest) { const token = request.cookies.get('token')?.value || ''; const body = await request.json(); - console.log('Create Article'); - - console.log(token); - console.log(body); - const res = await http.post('/articles', body, { headers: { 'Content-Type': 'application/json; charset=utf-8', @@ -18,8 +13,6 @@ export async function POST(request: NextRequest) { }, }); - console.log(res); - return NextResponse.json({ message: 'Create Article Success', success: true, data: res }); } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 400 }); diff --git a/components/article/ArticleList.tsx b/components/article/ArticleList.tsx index da60ad64..b33877fe 100644 --- a/components/article/ArticleList.tsx +++ b/components/article/ArticleList.tsx @@ -5,28 +5,21 @@ import React, { useRef } from 'react'; import { flexCenter } from '@/styles/common.css'; import useCurrentTab from '@/stores/useCurrentTab'; import useArticles from '@/hooks/useArticles'; -import useUserStore from '@/stores/useUserStore'; -import { User } from '@/types'; type Props = { username: string; }; const ArticleList = ({ username }: Props) => { const targetRef = useRef(null); - // const { username } = useUserStore() as User; const { tab } = useCurrentTab(); - const { data } = useArticles(targetRef, tab, username); - - console.log('List'); - - console.log(data); + const { articlesData } = useArticles({ targetRef, tab, username }); return (
- {data?.pages?.at(0)?.articles?.length === 0 ? ( + {articlesData?.pages?.at(0)?.articles?.length === 0 ? ( '데이터가 없습니다.' ) : (
- {data?.pages.map((group, i) => ( + {articlesData?.pages.map((group, i) => (
{group?.articles?.map(article => )}
diff --git a/components/article/ArticlePreview.tsx b/components/article/ArticlePreview.tsx index 8ec0648a..7b30fedb 100644 --- a/components/article/ArticlePreview.tsx +++ b/components/article/ArticlePreview.tsx @@ -5,8 +5,9 @@ import TagList from '../tags/TagList'; import { useRouter } from 'next/navigation'; import { FillHeartIcon } from '@/composables/icons'; import { fillGreenButton, flex, flexBetween, greenButton } from '@/styles/common.css'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; import useCurrentTab from '@/stores/useCurrentTab'; +import useArticles from '@/hooks/useArticles'; type Props = { article: any; @@ -19,36 +20,32 @@ const ArticlePreview = ({ const queryClient = useQueryClient(); - const { mutate } = useMutation({ - mutationFn: async (slug: string) => { - const method = favorited ? 'DELETE' : 'POST'; - console.log(slug); + const onSuccess = (res: any) => { + console.log(res); + queryClient.invalidateQueries({ queryKey: ['articles', tab] }); + }; - return fetch(`/api/articles/favorite/${slug}`, { method }).then(res => res.json()); - }, - onError: err => { - // 권한이 없을 경우 login 페이지로 이동 - console.log(err); + const onError = (err: any) => { + // 권한이 없을 경우 login 페이지로 이동 + console.log(err); + router.push('/login'); + }; + + const { favorite, unFavorite } = useArticles({ onSuccess, onError }); + + const handleButtonClick = (slug: string) => { + if (favorited) { + unFavorite(slug); + } else { + favorite(slug); + } + }; - router.push('/login'); - }, - onSuccess: data => { - console.log('성공 후 리패치'); - console.log(data); - queryClient.invalidateQueries({ queryKey: ['articles', tab] }); - // queryClient.setQueryData(['articles', tab], (oldQueryData: any) => { - // return { - // ...oldQueryData, - // data: [...oldQueryData.data, data?.data], - // }; - // }); - }, - }); return (
-
); diff --git a/components/profile/ProfileBox.tsx b/components/profile/ProfileBox.tsx new file mode 100644 index 00000000..559e0646 --- /dev/null +++ b/components/profile/ProfileBox.tsx @@ -0,0 +1,33 @@ +import { SettingIcon } from '@/composables/icons'; +import useUserStore from '@/stores/useUserStore'; +import { settingButton, userBlock, userImage, userInfo, userName } from '@/styles/profile.css'; +import { User } from '@/types'; +import Image from 'next/image'; +import Link from 'next/link'; +import FollowButton from '../user/FollowButton'; +type Props = { + image: string; + username: string; + following: boolean; +}; +const ProfileBox = ({ image, username, following }: Props) => { + const { username: currentUsername } = useUserStore() as User; + return ( +
+
+ Profile +
{username}
+ {currentUsername === username ? ( + + +   Edit Profile Settings + + ) : ( + + )} +
+
+ ); +}; + +export default ProfileBox; diff --git a/components/user/FollowButton.tsx b/components/user/FollowButton.tsx index c3f6184b..25fe357c 100644 --- a/components/user/FollowButton.tsx +++ b/components/user/FollowButton.tsx @@ -7,7 +7,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; type Props = { author: any; - slug: string; + slug?: string; }; const FollowButton = ({ author: { username, following }, slug }: Props) => { const router = useRouter(); diff --git a/hooks/useProfile.ts b/hooks/useProfile.ts index b273b018..dc9e90da 100644 --- a/hooks/useProfile.ts +++ b/hooks/useProfile.ts @@ -6,8 +6,8 @@ const useProfile = ({ onError, }: { username?: string; - onSuccess: (res: any) => void; - onError: (err: any) => void; + onSuccess?: (res: any) => void; + onError?: (err: any) => void; }) => { const { data: profile } = useQuery({ queryKey: ['profile', username], From d98a93d3ede3a3dd95680a8a6e23b07ff6f2b0b5 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Sun, 8 Oct 2023 15:24:54 +0900 Subject: [PATCH 45/49] =?UTF-8?q?fix:=20follow=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/articles.ts | 0 api/http/httpClient.ts | 43 +++++++++++++++++++++++++++++ app/article/[slug]/page.tsx | 13 +++++++-- app/editor/[slug]/page.tsx | 5 ++++ app/editor/page.tsx | 2 +- {assets => app}/favicon.ico | Bin app/layout.tsx | 3 ++ components/article/ArticleList.tsx | 2 +- components/user/FollowButton.tsx | 8 +++--- constants/api.ts | 10 +++++++ hooks/useArticles.ts | 2 +- hooks/useProfile.ts | 4 +-- 12 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 api/articles.ts create mode 100644 api/http/httpClient.ts rename {assets => app}/favicon.ico (100%) create mode 100644 constants/api.ts diff --git a/api/articles.ts b/api/articles.ts new file mode 100644 index 00000000..e69de29b diff --git a/api/http/httpClient.ts b/api/http/httpClient.ts new file mode 100644 index 00000000..b00def70 --- /dev/null +++ b/api/http/httpClient.ts @@ -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(); diff --git a/app/article/[slug]/page.tsx b/app/article/[slug]/page.tsx index b7af5a5d..4cfc3374 100644 --- a/app/article/[slug]/page.tsx +++ b/app/article/[slug]/page.tsx @@ -57,8 +57,17 @@ const ArticlePage = ({ params: { slug } }: Props) => {
-   - + {author.username === username ? ( +
+ + +
+ ) : ( +
+ + +
+ )}
diff --git a/app/editor/[slug]/page.tsx b/app/editor/[slug]/page.tsx index 854d3d93..fccd6035 100644 --- a/app/editor/[slug]/page.tsx +++ b/app/editor/[slug]/page.tsx @@ -1,8 +1,13 @@ +'use client'; import EditForm from '@/components/editor/EditForm'; type Props = { params: { slug: string }; }; const EditorUpdatePage = ({ params: { slug } }: Props) => { + console.log('update'); + + console.log(slug); + return (
diff --git a/app/editor/page.tsx b/app/editor/page.tsx index 453615d7..ddf5e422 100644 --- a/app/editor/page.tsx +++ b/app/editor/page.tsx @@ -29,7 +29,7 @@ const EditorPage = () => { console.log(data); queryClient.invalidateQueries(['articles', 'global']); - router.push('router'); + router.push('/'); }, onError: (error: any) => { console.log('에러 발생'); diff --git a/assets/favicon.ico b/app/favicon.ico similarity index 100% rename from assets/favicon.ico rename to app/favicon.ico diff --git a/app/layout.tsx b/app/layout.tsx index ed88ad0f..fde1883f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,6 +8,9 @@ import Providers from '@/libs/Providers'; export const metadata: Metadata = { title: 'next world', description: 'next world by hyeon', + icons: { + icon: '/favicon.ico', + }, }; export default function RootLayout({ children }: { children: ReactNode }) { diff --git a/components/article/ArticleList.tsx b/components/article/ArticleList.tsx index b33877fe..a3034b46 100644 --- a/components/article/ArticleList.tsx +++ b/components/article/ArticleList.tsx @@ -6,7 +6,7 @@ import { flexCenter } from '@/styles/common.css'; import useCurrentTab from '@/stores/useCurrentTab'; import useArticles from '@/hooks/useArticles'; type Props = { - username: string; + username?: string; }; const ArticleList = ({ username }: Props) => { const targetRef = useRef(null); diff --git a/components/user/FollowButton.tsx b/components/user/FollowButton.tsx index 25fe357c..dbbd265d 100644 --- a/components/user/FollowButton.tsx +++ b/components/user/FollowButton.tsx @@ -25,16 +25,16 @@ const FollowButton = ({ author: { username, following }, slug }: Props) => { const { follow, unFollow } = useProfile({ onSuccess, onError }); - const handleButtonClick = () => { + const handleButtonClick = (username: string) => { if (following) { - unFollow(); + unFollow(username); } else { - follow(); + follow(username); } }; return ( - ); diff --git a/constants/api.ts b/constants/api.ts new file mode 100644 index 00000000..80d8dd57 --- /dev/null +++ b/constants/api.ts @@ -0,0 +1,10 @@ +export const HTTP_METHOD = { + GET: 'GET', + POST: 'POST', + PUT: 'PUT', + DELETE: 'DELETE', +}; + +export const COMMON_HEADERS = { + 'Content-Type': 'application/json; charset=utf-8', +}; diff --git a/hooks/useArticles.ts b/hooks/useArticles.ts index d26a7f82..a657495b 100644 --- a/hooks/useArticles.ts +++ b/hooks/useArticles.ts @@ -1,4 +1,4 @@ -import { getArticlesWithFavoritedAPI, getArticlesWithTagAPI } from '@/services/articles'; +import { getArticlesWithTagAPI } from '@/services/articles'; import { useInfiniteQuery, useMutation } from '@tanstack/react-query'; import { RefObject } from 'react'; import useIntersectionObserver from './useIntersectionObserver'; diff --git a/hooks/useProfile.ts b/hooks/useProfile.ts index dc9e90da..dc00cca4 100644 --- a/hooks/useProfile.ts +++ b/hooks/useProfile.ts @@ -17,7 +17,7 @@ const useProfile = ({ }); const { mutate: follow } = useMutation({ - mutationFn: async () => { + mutationFn: async (username: string) => { return fetch(`/api/profiles/${username}`, { method: 'POST' }).then(res => res.json()); }, onSuccess, @@ -25,7 +25,7 @@ const useProfile = ({ }); const { mutate: unFollow } = useMutation({ - mutationFn: async () => { + mutationFn: async (username: string) => { return fetch(`/api/profiles/${username}`, { method: 'DELETE' }).then(res => res.json()); }, onSuccess, From 33b5c7a0215ae266a6dfaf232e71d544d851a401 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Sun, 8 Oct 2023 15:56:32 +0900 Subject: [PATCH 46/49] =?UTF-8?q?feat=20&=20refactor:=20EditForm=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=99=80=20=EC=9E=91=EC=84=B1=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/article/[slug]/page.tsx | 1 - app/editor/[slug]/page.tsx | 4 - app/editor/page.tsx | 87 +--------------------- components/article/ArticleDeleteButton.tsx | 2 +- components/editor/EditForm.tsx | 7 +- 5 files changed, 7 insertions(+), 94 deletions(-) diff --git a/app/article/[slug]/page.tsx b/app/article/[slug]/page.tsx index 4cfc3374..fe854600 100644 --- a/app/article/[slug]/page.tsx +++ b/app/article/[slug]/page.tsx @@ -22,7 +22,6 @@ const ArticlePage = ({ params: { slug } }: Props) => { const { data: article } = useQuery({ queryKey: ['article', slug], queryFn: async () => await fetch(`/api/articles/${slug}`).then(res => res.json()), - enabled: !!slug, select: res => res.data.article, }); diff --git a/app/editor/[slug]/page.tsx b/app/editor/[slug]/page.tsx index fccd6035..79daed01 100644 --- a/app/editor/[slug]/page.tsx +++ b/app/editor/[slug]/page.tsx @@ -4,10 +4,6 @@ type Props = { params: { slug: string }; }; const EditorUpdatePage = ({ params: { slug } }: Props) => { - console.log('update'); - - console.log(slug); - return (
diff --git a/app/editor/page.tsx b/app/editor/page.tsx index ddf5e422..8b7af91a 100644 --- a/app/editor/page.tsx +++ b/app/editor/page.tsx @@ -1,91 +1,10 @@ -'use client'; -import TagInput from '@/components/editor/TagInput'; -import { articleTextarea } from '@/styles/article.css'; -import { container, input } from '@/styles/common.css'; -import { editorForm, editorButton } from '@/styles/editor.css'; -import { NewArticle } from '@/types'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useRouter } from 'next/navigation'; -import React, { useState } from 'react'; +import EditForm from '@/components/editor/EditForm'; +import { container } from '@/styles/common.css'; const EditorPage = () => { - const queryClient = useQueryClient(); - const router = useRouter(); - - const [formData, setFormData] = useState({ - title: '', - description: '', - body: '', - tagList: [], - }); - - const { mutate } = useMutation({ - mutationFn: () => - fetch('/api/articles/new', { method: 'POST', body: JSON.stringify({ article: formData }) }).then(res => - res.json() - ), - onSuccess: data => { - console.log('등록 성공'); - - console.log(data); - queryClient.invalidateQueries(['articles', 'global']); - router.push('/'); - }, - onError: (error: any) => { - console.log('에러 발생'); - console.error(error); - }, - }); - - const handleClick = () => { - mutate(); - }; - - const handleChange = (e: any) => { - setFormData(prev => ({ - ...prev, - [e.target.name]: e.target.value, - })); - }; - - const appendTag = (tag: string) => { - setFormData(prevData => ({ ...prevData, tagList: [...prevData.tagList, tag] })); - }; return (
-
- - - - - -
- -
-
+
); }; diff --git a/components/article/ArticleDeleteButton.tsx b/components/article/ArticleDeleteButton.tsx index 6a5eab6f..496f391d 100644 --- a/components/article/ArticleDeleteButton.tsx +++ b/components/article/ArticleDeleteButton.tsx @@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation'; const ArticleDeleteButton = ({ slug }: { slug: string }) => { const router = useRouter(); const handleButtonClick = async () => { - const res = fetch(`/api/articles/${slug}`, { method: 'DELETE' }).then(res => res.json()); + const res = await fetch(`/api/articles/${slug}`, { method: 'DELETE' }).then(res => res.json()); console.log(res); router.push('/'); }; diff --git a/components/editor/EditForm.tsx b/components/editor/EditForm.tsx index daad4671..684d8ccb 100644 --- a/components/editor/EditForm.tsx +++ b/components/editor/EditForm.tsx @@ -8,17 +8,16 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { NewArticle } from '@/types'; import { useRouter } from 'next/navigation'; -import { getArticleAPI } from '@/services/articles'; -const EditForm = ({ slug }: { slug: string }) => { +const EditForm = ({ slug }: { slug?: string }) => { const queryClient = useQueryClient(); const router = useRouter(); const { data: article } = useQuery({ queryKey: ['article', slug], - queryFn: async () => await getArticleAPI(slug), + queryFn: async () => await fetch(`/api/articles/${slug}`).then(res => res.json()), enabled: !!slug, - select: res => res.article, + select: res => res.data.article, }); const [formData, setFormData] = useState({ From 072099338738baeec71069d5fe9d065c6470f0b3 Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Sun, 8 Oct 2023 16:52:28 +0900 Subject: [PATCH 47/49] =?UTF-8?q?fix:=20ArticlesList=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/page.tsx | 3 +-- hooks/useArticles.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 6028fb69..06480169 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,3 @@ -import ArticleList from '@/components/article/ArticleList'; - import SideBar from '@/components/layouts/SideBar'; import { articleContainer } from '@/styles/article.css'; @@ -9,6 +7,7 @@ import dynamic from 'next/dynamic'; import { Suspense } from 'react'; const ArticleTab = dynamic(() => import('@/components/article/ArticleTab'), { ssr: false }); +const ArticleList = dynamic(() => import('@/components/article/ArticleList'), { ssr: false }); const Banner = dynamic(() => import('@/components/layouts/Banner'), { ssr: false }); export default function Page() { diff --git a/hooks/useArticles.ts b/hooks/useArticles.ts index a657495b..947d561c 100644 --- a/hooks/useArticles.ts +++ b/hooks/useArticles.ts @@ -26,7 +26,7 @@ const useArticles = ({ queryFn: async ({ pageParam = 0 }) => { switch (tab) { case 'global': - return await fetch(`/api/articles?page=${pageParam}`).then(res => res.json()); + return await fetch(`http://localhost:3000/api/articles?page=${pageParam}`).then(res => res.json()); case 'my': return await fetch(`/api/articles/my?username=${username}&page=${pageParam}`).then(res => res.json()); case 'favorited': From ec99a309a987473aa37f7b81287245080c08c22d Mon Sep 17 00:00:00 2001 From: hyeon9782 Date: Sun, 8 Oct 2023 18:54:17 +0900 Subject: [PATCH 48/49] =?UTF-8?q?feat:=20Type=20=EC=84=B8=EB=B6=84?= =?UTF-8?q?=ED=99=94=20=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/articles.ts | 0 app/api/comments/[slug]/route.ts | 15 ---- app/api/user/route.ts | 10 +-- app/loading.tsx | 2 - components/account/SignInForm.tsx | 9 ++- components/account/SignUpForm.tsx | 8 +- components/article/ArticleDeleteButton.tsx | 3 +- components/article/ArticleList.tsx | 3 +- components/article/ArticlePreview.tsx | 6 +- components/article/ArticleTab.tsx | 2 +- components/comments/CommentBox.tsx | 7 +- components/comments/CommentCard.tsx | 6 +- components/comments/CommentForm.tsx | 2 +- components/editor/EditForm.tsx | 9 +-- components/editor/TagInput.tsx | 2 - components/layouts/Banner.tsx | 7 +- components/layouts/NavBar.tsx | 2 +- components/profile/ProfileBox.tsx | 2 +- components/settings/LogoutButton.tsx | 7 +- components/settings/SettingForm.tsx | 4 +- components/user/FollowButton.tsx | 6 +- hooks/useArticles.ts | 14 ++-- hooks/useAuth.ts | 19 +++-- services/tags.ts | 2 +- services/users.ts | 2 +- stores/useUserStore.ts | 3 +- types/api/articles.d.ts | 18 +++++ types/api/comment.d.ts | 21 +++++ types/api/profile.d.ts | 6 ++ types/api/users.d.ts | 18 +++++ types/index.ts | 93 ---------------------- types/props/composables.d.ts | 13 +++ types/store/userStore.d.ts | 7 ++ utils/http.ts | 2 + 34 files changed, 152 insertions(+), 178 deletions(-) delete mode 100644 api/articles.ts create mode 100644 types/api/articles.d.ts create mode 100644 types/api/comment.d.ts create mode 100644 types/api/profile.d.ts create mode 100644 types/api/users.d.ts delete mode 100644 types/index.ts create mode 100644 types/props/composables.d.ts create mode 100644 types/store/userStore.d.ts diff --git a/api/articles.ts b/api/articles.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/app/api/comments/[slug]/route.ts b/app/api/comments/[slug]/route.ts index c7c9cb55..150ef653 100644 --- a/app/api/comments/[slug]/route.ts +++ b/app/api/comments/[slug]/route.ts @@ -13,8 +13,6 @@ async function GET(req: NextRequest, route: { params: { slug: string } }) { }, }); - console.log(res); - return NextResponse.json({ message: 'Comment Get Success', success: true, data: res }); } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 400 }); @@ -26,10 +24,6 @@ async function POST(req: NextRequest, route: { params: { slug: string } }) { const body = await req.json(); const slug = route.params.slug; const token = req.cookies.get('token')?.value || ''; - console.log(body); - - console.log(slug); - console.log(token); const res = await http.post(`/articles/${slug}/comments`, body, { headers: { @@ -38,8 +32,6 @@ async function POST(req: NextRequest, route: { params: { slug: string } }) { }, }); - console.log(res); - return NextResponse.json({ message: 'Comment Create Success', success: true, data: res }); } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 400 }); @@ -55,19 +47,12 @@ async function DELETE(req: NextRequest, route: { params: { slug: string } }) { const token = req.cookies.get('token')?.value || ''; - console.log(slug); - console.log(token); - console.log('삭제 전'); - const res = await http.delete(`/articles/${slug}/comments/${id}`, { headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: `Token ${token}`, }, }); - console.log('삭제 후'); - - console.log(res); return NextResponse.json({ message: 'Comment Delete Success', success: true, data: res }); } catch (error: any) { diff --git a/app/api/user/route.ts b/app/api/user/route.ts index 3bcacb5c..c5525f61 100644 --- a/app/api/user/route.ts +++ b/app/api/user/route.ts @@ -2,9 +2,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { getUserAPI, updateUserAPI } from '@/services/users'; export async function GET(req: NextRequest) { - const { value } = req.cookies.get('token'); + const token = req.cookies.get('token')?.value || ''; - const { user } = await getUserAPI(value); + const { user } = await getUserAPI(token); return NextResponse.json({ message: 'Login successfull', @@ -14,10 +14,8 @@ export async function GET(req: NextRequest) { } export async function PUT(req: NextRequest) { - const { value } = req.cookies.get('token'); + const token = req.cookies.get('token')?.value || ''; const user = await req.json(); - console.log('Route'); - console.log(user); - return updateUserAPI(user, value); + return updateUserAPI(user, token); } diff --git a/app/loading.tsx b/app/loading.tsx index a359df7d..20ffa831 100644 --- a/app/loading.tsx +++ b/app/loading.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - const HomeLoading = () => { return
HomeLoading...
; }; diff --git a/components/account/SignInForm.tsx b/components/account/SignInForm.tsx index de88fb02..074143ae 100644 --- a/components/account/SignInForm.tsx +++ b/components/account/SignInForm.tsx @@ -4,7 +4,8 @@ import useUserStore from '@/stores/useUserStore'; import { form } from '@/styles/account.css'; import { fillGreenButton, input } from '@/styles/common.css'; import { buttonBox } from '@/styles/layout.css'; -import { LoginUser, UserAction } from '@/types'; +import { LoginUser, UserResponse } from '@/types/api/users'; +import { UserAction } from '@/types/store/userStore'; import { useRouter } from 'next/navigation'; import { ChangeEvent, FormEvent, useState } from 'react'; @@ -17,13 +18,13 @@ const SignInForm = () => { password: '', }); - const loginSuccess = (res: any) => { + const loginSuccess = (res: UserResponse) => { saveUserInfo({ ...res.user }); router.push('/'); }; - const loginError = (err: any) => { - console.log(err.message); + const loginError = (err: Error) => { + console.error(err.message); alert('이메일 또는 비밀번호가 잘못되었습니다.'); }; diff --git a/components/account/SignUpForm.tsx b/components/account/SignUpForm.tsx index 5efc6074..d8adefd0 100644 --- a/components/account/SignUpForm.tsx +++ b/components/account/SignUpForm.tsx @@ -5,7 +5,9 @@ import useUserStore from '@/stores/useUserStore'; import { form } from '@/styles/account.css'; import { fillGreenButton, input } from '@/styles/common.css'; import { buttonBox } from '@/styles/layout.css'; -import { NewUser, UserAction } from '@/types'; +import { NewUser } from '@/types/api/users'; +import { UserAction } from '@/types/store/userStore'; + import { useRouter } from 'next/navigation'; import { ChangeEvent, FormEvent, useState } from 'react'; @@ -19,10 +21,6 @@ const SignUpForm = () => { }); const signupSuccess = (res: any) => { - console.log('Client'); - - console.log(res); - saveUserInfo({ ...res.user, }); diff --git a/components/article/ArticleDeleteButton.tsx b/components/article/ArticleDeleteButton.tsx index 496f391d..9df24e7e 100644 --- a/components/article/ArticleDeleteButton.tsx +++ b/components/article/ArticleDeleteButton.tsx @@ -5,8 +5,7 @@ import { useRouter } from 'next/navigation'; const ArticleDeleteButton = ({ slug }: { slug: string }) => { const router = useRouter(); const handleButtonClick = async () => { - const res = await fetch(`/api/articles/${slug}`, { method: 'DELETE' }).then(res => res.json()); - console.log(res); + await fetch(`/api/articles/${slug}`, { method: 'DELETE' }).then(res => res.json()); router.push('/'); }; return ( diff --git a/components/article/ArticleList.tsx b/components/article/ArticleList.tsx index a3034b46..33c175b0 100644 --- a/components/article/ArticleList.tsx +++ b/components/article/ArticleList.tsx @@ -5,6 +5,7 @@ import React, { useRef } from 'react'; import { flexCenter } from '@/styles/common.css'; import useCurrentTab from '@/stores/useCurrentTab'; import useArticles from '@/hooks/useArticles'; +import { Article } from '@/types/api/articles'; type Props = { username?: string; }; @@ -21,7 +22,7 @@ const ArticleList = ({ username }: Props) => {
{articlesData?.pages.map((group, i) => (
- {group?.articles?.map(article => )} + {group?.articles?.map((article: Article) => )}
))}
diff --git a/components/article/ArticlePreview.tsx b/components/article/ArticlePreview.tsx index 7b30fedb..8e85d993 100644 --- a/components/article/ArticlePreview.tsx +++ b/components/article/ArticlePreview.tsx @@ -20,14 +20,12 @@ const ArticlePreview = ({ const queryClient = useQueryClient(); - const onSuccess = (res: any) => { - console.log(res); + const onSuccess = () => { queryClient.invalidateQueries({ queryKey: ['articles', tab] }); }; - const onError = (err: any) => { + const onError = () => { // 권한이 없을 경우 login 페이지로 이동 - console.log(err); router.push('/login'); }; diff --git a/components/article/ArticleTab.tsx b/components/article/ArticleTab.tsx index 9907b391..f6d60c22 100644 --- a/components/article/ArticleTab.tsx +++ b/components/article/ArticleTab.tsx @@ -2,7 +2,7 @@ import useCurrentTab from '@/stores/useCurrentTab'; import useUserStore from '@/stores/useUserStore'; import { articleTab, articleTabItem, articleTabItemActivate, articleTabItemDisable } from '@/styles/article.css'; -import { User } from '@/types'; +import { User } from '@/types/api/users'; import { usePathname } from 'next/navigation'; import { useEffect } from 'react'; diff --git a/components/comments/CommentBox.tsx b/components/comments/CommentBox.tsx index b629d448..69a01c84 100644 --- a/components/comments/CommentBox.tsx +++ b/components/comments/CommentBox.tsx @@ -5,8 +5,9 @@ import React from 'react'; import CommentForm from './CommentForm'; import CommentCard from './CommentCard'; import { flexCenter, flexRow, textCenter } from '@/styles/common.css'; -import { User } from '@/types'; import { useQuery } from '@tanstack/react-query'; +import { User } from '@/types/api/users'; +import { Comment } from '@/types/api/comment'; const CommentBox = ({ slug }: { slug: string }) => { const { email } = useUserStore() as User; @@ -16,15 +17,13 @@ const CommentBox = ({ slug }: { slug: string }) => { select: res => res.data.comments, }); - console.log(comments); - return (
{email ? (
- {comments.map(comment => ( + {comments.map((comment: Comment) => ( ))}
diff --git a/components/comments/CommentCard.tsx b/components/comments/CommentCard.tsx index 4b7f3d21..46b0e4f3 100644 --- a/components/comments/CommentCard.tsx +++ b/components/comments/CommentCard.tsx @@ -4,7 +4,9 @@ import { TrashIcon } from '@/composables/icons'; import useUserStore from '@/stores/useUserStore'; import { commentContent, commentFormFooter, commnetCard } from '@/styles/comments.css'; import { circle, flexCenter } from '@/styles/common.css'; -import { Comment, User } from '@/types'; +import { Comment } from '@/types/api/comment'; +import { User } from '@/types/api/users'; + import { formatDate } from '@/utils'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import Image from 'next/image'; @@ -37,7 +39,7 @@ const CommentCard = ({ comment, slug }: Props) => { height={20} alt="iamge" /> -   {comment.author.username} {formatDate(comment.createAt)} +   {comment.author.username} {formatDate(comment.createdAt)}
{comment.author.username === username && handleTrashClick(slug)} />}
diff --git a/components/comments/CommentForm.tsx b/components/comments/CommentForm.tsx index 9651f6f5..8ec3d31c 100644 --- a/components/comments/CommentForm.tsx +++ b/components/comments/CommentForm.tsx @@ -24,7 +24,6 @@ const CommentForm = ({ slug }: { slug: string }) => { }); const handleSubmit = (e: any) => { e.preventDefault(); - console.log(comment); mutate(e.target.comment.value); setComment(''); @@ -36,6 +35,7 @@ const CommentForm = ({ slug }: { slug: string }) => { name="comment" className={commentTextarea} placeholder="Write a comment..." + value={comment} onChange={e => setComment(e.target.value)} >
diff --git a/components/editor/EditForm.tsx b/components/editor/EditForm.tsx index 684d8ccb..77ed6136 100644 --- a/components/editor/EditForm.tsx +++ b/components/editor/EditForm.tsx @@ -6,8 +6,9 @@ import { editorButton, editorForm } from '@/styles/editor.css'; import TagInput from './TagInput'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; -import { NewArticle } from '@/types'; + import { useRouter } from 'next/navigation'; +import { NewArticle } from '@/types/api/articles'; const EditForm = ({ slug }: { slug?: string }) => { const queryClient = useQueryClient(); @@ -39,15 +40,11 @@ const EditForm = ({ slug }: { slug?: string }) => { ); } }, - onSuccess: data => { - console.log('등록 성공'); - - console.log(data); + onSuccess: () => { queryClient.invalidateQueries(['articles', 'global']); router.push('/'); }, onError: (error: any) => { - console.log('에러 발생'); console.error(error); }, }); diff --git a/components/editor/TagInput.tsx b/components/editor/TagInput.tsx index a2797a27..55232935 100644 --- a/components/editor/TagInput.tsx +++ b/components/editor/TagInput.tsx @@ -21,8 +21,6 @@ const TagInput = ({ appendTag }: Props) => { }; const handleTagClick = (tag: string) => { - console.log('쿨릭'); - setTags((prevTags: string[]) => prevTags.filter(prevTag => prevTag !== tag)); }; diff --git a/components/layouts/Banner.tsx b/components/layouts/Banner.tsx index 5f2c27dd..fd1ff029 100644 --- a/components/layouts/Banner.tsx +++ b/components/layouts/Banner.tsx @@ -2,7 +2,8 @@ import useUserStore from '@/stores/useUserStore'; import { backgroundBlack, backgroundGreen, container } from '@/styles/common.css'; import { banner } from '@/styles/home.css'; -import { User } from '@/types'; +import { User } from '@/types/api/users'; + import { ReactNode } from 'react'; type Props = { children: ReactNode; @@ -11,7 +12,9 @@ type Props = { const Banner = ({ children, background }: Props) => { const { email } = useUserStore() as User; return email && background === 'green' ? ( -
+
+
+
) : (
{children}
diff --git a/components/layouts/NavBar.tsx b/components/layouts/NavBar.tsx index 6a14c5cc..d13b5dc5 100644 --- a/components/layouts/NavBar.tsx +++ b/components/layouts/NavBar.tsx @@ -6,7 +6,7 @@ import { userImageSm } from '@/styles/profile.css'; import Image from 'next/image'; import { EditIcon, SettingIcon } from '@/composables/icons'; import useUserStore from '@/stores/useUserStore'; -import { User } from '@/types'; +import { User } from '@/types/api/users'; const NAVS = [ { diff --git a/components/profile/ProfileBox.tsx b/components/profile/ProfileBox.tsx index 559e0646..c8ac50f6 100644 --- a/components/profile/ProfileBox.tsx +++ b/components/profile/ProfileBox.tsx @@ -1,10 +1,10 @@ import { SettingIcon } from '@/composables/icons'; import useUserStore from '@/stores/useUserStore'; import { settingButton, userBlock, userImage, userInfo, userName } from '@/styles/profile.css'; -import { User } from '@/types'; import Image from 'next/image'; import Link from 'next/link'; import FollowButton from '../user/FollowButton'; +import { User } from '@/types/api/users'; type Props = { image: string; username: string; diff --git a/components/settings/LogoutButton.tsx b/components/settings/LogoutButton.tsx index 404c0439..60896e9c 100644 --- a/components/settings/LogoutButton.tsx +++ b/components/settings/LogoutButton.tsx @@ -3,18 +3,19 @@ import useAuth from '@/hooks/useAuth'; import useUserStore from '@/stores/useUserStore'; import { flex } from '@/styles/common.css'; import { logoutButton } from '@/styles/settings.css'; -import { UserAction } from '@/types'; +import { UserAction } from '@/types/store/userStore'; + import { useRouter } from 'next/navigation'; const LogoutButton = () => { const router = useRouter(); const { logout } = useUserStore() as UserAction; - const signoutSuccess = (res: any) => { + const signoutSuccess = () => { logout(); router.push('/login'); }; - const signoutError = (err: any) => { + const signoutError = (err: Error) => { console.error(err.message); }; diff --git a/components/settings/SettingForm.tsx b/components/settings/SettingForm.tsx index a28cc249..188a5e3f 100644 --- a/components/settings/SettingForm.tsx +++ b/components/settings/SettingForm.tsx @@ -24,9 +24,7 @@ const SettingForm = () => { const { mutate, isLoading } = useMutation({ mutationFn: (formData: any) => fetch('/api/auth/user', { method: 'PUT', body: JSON.stringify(formData) }).then(res => res.json()), - onSuccess: data => { - console.log(data); - console.log('성공'); + onSuccess: () => { updateUser({ ...formData, }); diff --git a/components/user/FollowButton.tsx b/components/user/FollowButton.tsx index dbbd265d..490f7fa0 100644 --- a/components/user/FollowButton.tsx +++ b/components/user/FollowButton.tsx @@ -13,13 +13,11 @@ const FollowButton = ({ author: { username, following }, slug }: Props) => { const router = useRouter(); const queryClient = useQueryClient(); - const onSuccess = (res: any) => { - console.log(res); + const onSuccess = () => { queryClient.invalidateQueries(['article', slug]); }; - const onError = (err: any) => { - console.error(err); + const onError = () => { router.push('/login'); }; diff --git a/hooks/useArticles.ts b/hooks/useArticles.ts index 947d561c..3387791c 100644 --- a/hooks/useArticles.ts +++ b/hooks/useArticles.ts @@ -13,8 +13,8 @@ const useArticles = ({ targetRef?: RefObject | undefined; tab?: string; username?: string; - onSuccess?: (res: any) => void; - onError?: (err: any) => void; + onSuccess?: (res?: any) => void; + onError?: (err?: any) => void; }) => { const { data: articlesData, @@ -28,11 +28,15 @@ const useArticles = ({ case 'global': return await fetch(`http://localhost:3000/api/articles?page=${pageParam}`).then(res => res.json()); case 'my': - return await fetch(`/api/articles/my?username=${username}&page=${pageParam}`).then(res => res.json()); + return await fetch(`http://localhost:3000/api/articles/my?username=${username}&page=${pageParam}`).then(res => + res.json() + ); case 'favorited': - return await fetch(`/api/articles?username=${username}&page=${pageParam}`).then(res => res.json()); + return await fetch(`http://localhost:3000/api/articles?username=${username}&page=${pageParam}`).then(res => + res.json() + ); case 'your': - return await fetch(`/api/articles/feed?page=${pageParam}`).then(res => res.json()); + return await fetch(`http://localhost:3000/api/articles/feed?page=${pageParam}`).then(res => res.json()); default: return await getArticlesWithTagAPI(tab, pageParam); } diff --git a/hooks/useAuth.ts b/hooks/useAuth.ts index 4496a32f..fa8bdf6f 100644 --- a/hooks/useAuth.ts +++ b/hooks/useAuth.ts @@ -1,4 +1,4 @@ -import { LoginUser, NewUser } from '@/types'; +import { LoginUser, NewUser, UserResponse } from '@/types/api/users'; import { useMutation } from '@tanstack/react-query'; // 로그인 / 로그아웃 / 회원가입 / 로그인 확인 const useAuth = ({ @@ -9,13 +9,16 @@ const useAuth = ({ signoutSuccess, signoutError, }: { - loginSuccess?: (res: any) => void; - loginError?: (err: any) => void; - signupSuccess?: (res: any) => void; - signupError?: (err: any) => void; - signoutSuccess?: (res: any) => void; - signoutError?: (err: any) => void; + loginSuccess?: (res: UserResponse) => void; + loginError?: (err: Error) => void; + signupSuccess?: (res: UserResponse) => void; + signupError?: (err: Error) => void; + signoutSuccess?: (res: UserResponse) => void; + signoutError?: (err: Error) => void; }) => { + // 그냥 onSuccess랑 onError로 다 해결할까? login, signup, signout을 같은 컴포넌트에서 + // 사용할 일이 있을까? + // 로그인 const { mutate: login } = useMutation({ mutationFn: async (loginUser: LoginUser) => @@ -36,7 +39,7 @@ const useAuth = ({ // 로그아웃 const { mutate: signOut } = useMutation({ - mutationFn: () => fetch('/api/auth/logout').then(res => res.json()), + mutationFn: async () => await fetch('/api/auth/logout').then(res => res.json()), onSuccess: signoutSuccess, onError: signoutError, }); diff --git a/services/tags.ts b/services/tags.ts index 9af92b31..79ed2801 100644 --- a/services/tags.ts +++ b/services/tags.ts @@ -1,5 +1,5 @@ import { http } from '@/utils/http'; export const getTags = async () => { - return http.get('/tags'); + return await http.get('/tags'); }; diff --git a/services/users.ts b/services/users.ts index c0e8c6b2..6ea24633 100644 --- a/services/users.ts +++ b/services/users.ts @@ -1,4 +1,4 @@ -import { LoginUser, NewUser, UpdateUser } from '@/types'; +import { LoginUser, NewUser, UpdateUser } from '@/types/api/users'; import { http } from '@/utils/http'; // 회원가입 diff --git a/stores/useUserStore.ts b/stores/useUserStore.ts index 64e543ba..c5c10f93 100644 --- a/stores/useUserStore.ts +++ b/stores/useUserStore.ts @@ -1,4 +1,5 @@ -import { User, UserAction } from '@/types'; +import { User } from '@/types/api/users'; +import { UserAction } from '@/types/store/userStore'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; diff --git a/types/api/articles.d.ts b/types/api/articles.d.ts new file mode 100644 index 00000000..8e8c59de --- /dev/null +++ b/types/api/articles.d.ts @@ -0,0 +1,18 @@ +import { Profile } from './profile'; + +export type Article = { + slug?: string; + title: string; + description?: string; + body: string; + tagList: string[]; + createdAt: string; + updateAt?: string; + favorited?: boolean; + favoritesCount: number; + author: Profile; +}; + +export type NewArticle = Pick; + +export type UpdateArticle = Pick; diff --git a/types/api/comment.d.ts b/types/api/comment.d.ts new file mode 100644 index 00000000..ae2dc8b1 --- /dev/null +++ b/types/api/comment.d.ts @@ -0,0 +1,21 @@ +import { Profile } from './profile'; + +export type Comment = { + id: number; + createdAt: string; + updatedAt: string; + // createdAt* [...] + // updatedAt + body: string; + author: Profile; +}; + +export type NewComment = Pick; + +export type CommentResponse = { + message: string; + success: boolean; + data: { + comments: Comment[]; + }; +}; diff --git a/types/api/profile.d.ts b/types/api/profile.d.ts new file mode 100644 index 00000000..5115420b --- /dev/null +++ b/types/api/profile.d.ts @@ -0,0 +1,6 @@ +export type Profile = { + username: string; + bio: string; + image: string; + following: boolean; +}; diff --git a/types/api/users.d.ts b/types/api/users.d.ts new file mode 100644 index 00000000..aeadf738 --- /dev/null +++ b/types/api/users.d.ts @@ -0,0 +1,18 @@ +export type User = { + email: string; + token?: string; + username: string; + bio: string; + image: string; + password?: string; +}; + +export type LoginUser = Pick; + +export type NewUser = Pick; + +export type UpdateUser = Omit; + +export type UserResponse = { + user: Omit; +}; diff --git a/types/index.ts b/types/index.ts deleted file mode 100644 index 2bf404fe..00000000 --- a/types/index.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { ReactNode } from 'react'; - -export type Button = { - size?: string; - onClick: () => void; - children: ReactNode; - type: string; -}; - -export type Input = { - size: string; - placeholder: string; -}; - -export type LoginUser = { - email: string; - password: string; -}; - -export type NewUser = { - username: string; - email: string; - password: string; -}; - -export type UpdateUser = { - email: string; - password: string; - username: string; - bio: string; - image: string; -}; - -export type Profile = { - usename: string; - bio: string; - image: string; - following: boolean; -}; - -export type Article = { - slug?: string; - title: string; - description?: string; - body: string; - tagList: string[]; - createdAt: string; - updateAt?: string; - favorited?: boolean; - favoritesCount: number; - author: Profile; -}; - -export type NewArticle = { - title: string; - description: string; - body: string; - tagList: string[]; -}; - -export type UpdateArticle = { - title: string; - description: string; - body: string; -}; - -export type Comment = { - id: number; - createdAt: string; - updatedAt: string; - body: string; - author: Profile; -}; - -export type User = { - email: string; - token?: string; - username: string; - bio: string; - image: string; - password?: string; -}; - -export type UserAction = { - saveUserInfo?: (user: User) => void; - updateUser?: (user: User) => void; - logout?: () => void; - reset?: () => void; -}; - -export type UserResponse = { - user: User; -}; diff --git a/types/props/composables.d.ts b/types/props/composables.d.ts new file mode 100644 index 00000000..d6274a07 --- /dev/null +++ b/types/props/composables.d.ts @@ -0,0 +1,13 @@ +import { ReactNode } from 'react'; + +export type Button = { + size?: string; + onClick: () => void; + children: ReactNode; + type: string; +}; + +export type Input = { + size: string; + placeholder: string; +}; diff --git a/types/store/userStore.d.ts b/types/store/userStore.d.ts new file mode 100644 index 00000000..fdd3c182 --- /dev/null +++ b/types/store/userStore.d.ts @@ -0,0 +1,7 @@ +import { User } from '../api/users'; + +export type UserAction = { + saveUserInfo: (user: User) => void; + updateUser: (user: User) => void; + logout: () => void; +}; diff --git a/utils/http.ts b/utils/http.ts index 1abc2a69..8edd6d4b 100644 --- a/utils/http.ts +++ b/utils/http.ts @@ -10,6 +10,8 @@ export const http = { ...options, }; console.log(defaultOptions); + console.log(API_BASE_URL); + try { const response = await fetch(`${API_BASE_URL}${url}`, defaultOptions); From 7f488cccffa19f5b5362615eb082ddd9a7419789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=A0=95=ED=98=B8=20Jeongho=20Hyeon?= <62998723+hyeon9782@users.noreply.github.com> Date: Wed, 18 Oct 2023 20:22:03 +0900 Subject: [PATCH 49/49] Update README.md --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4f1c8604..4cd3a771 100644 --- a/README.md +++ b/README.md @@ -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 +
+ + + +
-> Describe the general architecture of your app here +### Config -# Getting started + -> npm install, npm start, etc. +### Development + +
+ + + + + +
+ +## 페이지 구성 + +### 메인 페이지 (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 정리 (급하게 하느라 너무 막 짠 거 같습니다..) +- [ ] 테스트 코드 추가