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/.github/workflows/review-assign-action.yml b/.github/workflows/review-assign-action.yml
new file mode 100644
index 00000000..21608d55
--- /dev/null
+++ b/.github/workflows/review-assign-action.yml
@@ -0,0 +1,50 @@
+name: Review Assign
+
+on:
+ pull_request:
+ types: [opened, ready_for_review]
+
+jobs:
+ assign:
+ permissions:
+ actions: write
+ checks: write
+ contents: write
+ deployments: write
+ discussions: write
+ issues: write
+ id-token: read
+ packages: write
+ pages: write
+ pull-requests: write
+ repository-projects: write
+ security-events: write
+ statuses: write
+ runs-on: ubuntu-latest
+ steps:
+ - if: github.base_ref == 'main' # base branch name is 'master'
+ run: echo REVIEWERS=inseong-so >> $GITHUB_ENV
+ - if: startsWith(github.base_ref, 'team1')
+ run: echo REVIEWERS=headring, KimHunJin, hyjoong her0707 >> $GITHUB_ENV
+ - if: startsWith(github.base_ref, 'team2')
+ run: echo REVIEWERS=Bsfla, SeolJaeHyeok, choisy9619, kyung-jun >> $GITHUB_ENV
+ - if: startsWith(github.base_ref, 'team3')
+ run: echo REVIEWERS=sgsg9447, kingyong9169, 2dowon, jqkk >> $GITHUB_ENV
+ - if: startsWith(github.base_ref, 'team4')
+ run: echo REVIEWERS=kimseongchan-kr, cham0287, hyeon9782 >> $GITHUB_ENV
+ - if: startsWith(github.base_ref, 'team5')
+ run: echo REVIEWERS=2-NOW, hyew-kim, geeonie >> $GITHUB_ENV
+ - if: startsWith(github.base_ref, 'team6')
+ run: echo REVIEWERS=areumsheep, ludacirs, innocarpe >> $GITHUB_ENV
+ - if: startsWith(github.base_ref, 'team7')
+ run: echo REVIEWERS=endmoseung, steven-yn, ding-co, mandarin-sep >> $GITHUB_ENV
+ - if: startsWith(github.base_ref, 'team8')
+ run: echo REVIEWERS=HOJOON07, jiji-hoon96, 71summernight, seung-wan >> $GITHUB_ENV
+ - if: startsWith(github.base_ref, 'team9')
+ run: echo REVIEWERS=Siihyun, hhhminme, 0uizi0, brgndyy >> $GITHUB_ENV
+ - if: startsWith(github.base_ref, 'team10')
+ run: echo REVIEWERS=Leejha, steadily-worked >> $GITHUB_ENV
+ - uses: hkusu/review-assign-action@v1
+ with:
+ assignees: ${{ github.actor }}
+ reviewers: ${{ env.REVIEWERS }}
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/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 정리 (급하게 하느라 너무 막 짠 거 같습니다..)
+- [ ] 테스트 코드 추가
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/[slug]/page.tsx b/app/[slug]/page.tsx
deleted file mode 100644
index 3ef8d5e0..00000000
--- a/app/[slug]/page.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import ArticleTab from '@/components/article/ArticleTab';
-import { SettingIcon } from '@/composables/icons';
-import { container } from '@/styles/common.css';
-import { settingButton, userBlock, userImage, userInfo, userName } from '@/styles/profile.css';
-import Image from 'next/image';
-import Link from 'next/link';
-
-const ProfilePage = async () => {
- // const user = await fetchUser();
- return (
-
-
-
-
-
hyeon9782
-
-
- Edit Profile Settings
-
-
-
-
-
- );
-};
-
-export default ProfilePage;
diff --git a/app/[slug]/loading.tsx b/app/[username]/loading.tsx
similarity index 100%
rename from app/[slug]/loading.tsx
rename to app/[username]/loading.tsx
diff --git a/app/[username]/page.tsx b/app/[username]/page.tsx
new file mode 100644
index 00000000..9a1c16fc
--- /dev/null
+++ b/app/[username]/page.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import ArticleList from '@/components/article/ArticleList';
+import ProfileBox from '@/components/profile/ProfileBox';
+import useProfile from '@/hooks/useProfile';
+import { container } from '@/styles/common.css';
+import dynamic from 'next/dynamic';
+import { Suspense } from 'react';
+
+const ArticleTab = dynamic(() => import('@/components/article/ArticleTab'), { ssr: false });
+type Props = {
+ params: { username: string };
+};
+const ProfilePage = ({ params: { username } }: Props) => {
+ const { profile } = useProfile({ username });
+
+ return (
+
+ );
+};
+
+export default ProfilePage;
diff --git a/app/api/articles/[slug]/route.ts b/app/api/articles/[slug]/route.ts
new file mode 100644
index 00000000..e6cd446e
--- /dev/null
+++ b/app/api/articles/[slug]/route.ts
@@ -0,0 +1,60 @@
+import { http } from '@/utils/http';
+import { NextRequest, NextResponse } from 'next/server';
+
+async function GET(req: NextRequest, route: { params: { slug: string } }) {
+ try {
+ const slug = route.params.slug;
+ const token = req.cookies.get('token')?.value || '';
+
+ const res = await http.get(`/articles/${slug}`, {
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ Authorization: `Token ${token}`,
+ },
+ });
+
+ return NextResponse.json({ message: 'Article Get Success', success: true, data: res });
+ } catch (error: any) {
+ console.log(error);
+ return NextResponse.json({ error: error.message }, { status: 400 });
+ }
+}
+
+async function PUT(req: NextRequest, route: { params: { slug: string } }) {
+ try {
+ const body = await req.json();
+ const slug = route.params.slug;
+ const token = req.cookies.get('token')?.value || '';
+
+ const res = await http.put(`/articles/${slug}`, body, {
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ Authorization: `Token ${token}`,
+ },
+ });
+
+ return NextResponse.json({ message: 'Article Update Success', success: true, data: res });
+ } catch (error: any) {
+ return NextResponse.json({ error: error.message }, { status: 400 });
+ }
+}
+
+async function DELETE(req: NextRequest, route: { params: { slug: string } }) {
+ try {
+ const slug = route.params.slug;
+ const token = req.cookies.get('token')?.value || '';
+
+ const res = await http.delete(`/articles/${slug}`, {
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ Authorization: `Token ${token}`,
+ },
+ });
+
+ return NextResponse.json({ message: 'Article Delete Success', success: true, data: res });
+ } catch (error: any) {
+ return NextResponse.json({ error: error.message }, { status: 400 });
+ }
+}
+
+export { GET, PUT, DELETE };
diff --git a/app/api/articles/favorite/[slug]/route.ts b/app/api/articles/favorite/[slug]/route.ts
new file mode 100644
index 00000000..337dda24
--- /dev/null
+++ b/app/api/articles/favorite/[slug]/route.ts
@@ -0,0 +1,41 @@
+import { http } from '@/utils/http';
+import { NextRequest, NextResponse } from 'next/server';
+
+// 좋아요
+async function POST(request: NextRequest, route: { params: { slug: string } }) {
+ try {
+ const slug = route.params.slug;
+ const token = request.cookies.get('token')?.value || '';
+
+ const res = await http.post(`/articles/${slug}/favorite`, '', {
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ Authorization: `Token ${token}`,
+ },
+ });
+
+ return NextResponse.json({ message: 'Favorite Success', success: true, data: res });
+ } catch (error: any) {
+ return NextResponse.json({ error: error.message }, { status: 400 });
+ }
+}
+
+// 좋아요 취소
+async function DELETE(request: NextRequest, route: { params: { slug: string } }) {
+ try {
+ const slug = route.params.slug;
+ const token = request.cookies.get('token')?.value || '';
+ 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 });
+ }
+}
+
+export { POST, DELETE };
diff --git a/app/api/articles/feed/route.ts b/app/api/articles/feed/route.ts
new file mode 100644
index 00000000..db3baf6d
--- /dev/null
+++ b/app/api/articles/feed/route.ts
@@ -0,0 +1,14 @@
+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 || '';
+
+ const { articles, articlesCount } = await getArticlesFeed(Number(page), token);
+
+ return NextResponse.json({ articles, articlesCount });
+}
diff --git a/app/api/articles/my/route.ts b/app/api/articles/my/route.ts
new file mode 100644
index 00000000..be7aff7e
--- /dev/null
+++ b/app/api/articles/my/route.ts
@@ -0,0 +1,14 @@
+import { getArticlesWithAuthorAPI } from '@/services/articles';
+import { NextRequest, NextResponse } from 'next/server';
+
+export async function GET(request: NextRequest) {
+ const { searchParams } = new URL(request.url);
+
+ const username = searchParams.get('username') || '';
+
+ const token = request.cookies.get('token')?.value || '';
+
+ const { articles, articlesCount } = await getArticlesWithAuthorAPI(username, token);
+
+ return NextResponse.json({ articles, articlesCount });
+}
diff --git a/app/api/articles/new/route.ts b/app/api/articles/new/route.ts
new file mode 100644
index 00000000..e28ce3a4
--- /dev/null
+++ b/app/api/articles/new/route.ts
@@ -0,0 +1,20 @@
+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();
+
+ const res = await http.post('/articles', body, {
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ Authorization: `Token ${token}`,
+ },
+ });
+
+ 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/api/articles/route.ts b/app/api/articles/route.ts
index f19aa320..78fe09a2 100644
--- a/app/api/articles/route.ts
+++ b/app/api/articles/route.ts
@@ -1,15 +1,19 @@
-import { fetchArticlesWithTag, registerArticle } from '@/services/articles';
+import { getArticlesAPI, getArticlesWithFavoritedAPI } 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 GET(request: NextRequest) {
+ const { searchParams } = new URL(request.url);
+
+ const page = searchParams.get('page');
+ const username = searchParams.get('username');
-export async function POST(req: NextRequest) {
- console.log(req.body);
+ const token = request.cookies.get('token')?.value || '';
- const res = await registerArticle(req.body);
- return NextResponse.json(res);
+ if (username) {
+ const { articles, articlesCount } = await getArticlesWithFavoritedAPI(username, token, Number(page));
+ return NextResponse.json({ articles, articlesCount });
+ } else {
+ const { articles, articlesCount } = await getArticlesAPI(token, Number(page));
+ return NextResponse.json({ articles, articlesCount });
+ }
}
diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts
new file mode 100644
index 00000000..3cf4120d
--- /dev/null
+++ b/app/api/auth/login/route.ts
@@ -0,0 +1,25 @@
+import { loginAPI } from '@/services/users';
+import { NextRequest, NextResponse } from 'next/server';
+
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+
+ const res = await loginAPI(body);
+
+ 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/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..b51a384e
--- /dev/null
+++ b/app/api/auth/signup/route.ts
@@ -0,0 +1,23 @@
+import { registerUserAPI } from '@/services/users';
+import { NextRequest, NextResponse } from 'next/server';
+
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const res = await registerUserAPI(body);
+
+ 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/api/auth/user/route.ts b/app/api/auth/user/route.ts
new file mode 100644
index 00000000..88d3fe4b
--- /dev/null
+++ b/app/api/auth/user/route.ts
@@ -0,0 +1,25 @@
+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);
+ console.log(res);
+
+ 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/[slug]/route.ts b/app/api/comments/[slug]/route.ts
new file mode 100644
index 00000000..150ef653
--- /dev/null
+++ b/app/api/comments/[slug]/route.ts
@@ -0,0 +1,63 @@
+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}`,
+ },
+ });
+
+ 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 || '';
+
+ const res = await http.post(`/articles/${slug}/comments`, body, {
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ Authorization: `Token ${token}`,
+ },
+ });
+
+ 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 { searchParams } = new URL(req.url);
+
+ const id = searchParams.get('id');
+ const slug = route.params.slug;
+
+ const token = req.cookies.get('token')?.value || '';
+
+ const res = await http.delete(`/articles/${slug}/comments/${id}`, {
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ Authorization: `Token ${token}`,
+ },
+ });
+
+ 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/profiles/[username]/route.ts b/app/api/profiles/[username]/route.ts
new file mode 100644
index 00000000..96008131
--- /dev/null
+++ b/app/api/profiles/[username]/route.ts
@@ -0,0 +1,63 @@
+import { http } from '@/utils/http';
+import { NextRequest, NextResponse } from 'next/server';
+
+async function GET(req: NextRequest, route: { params: { username: string } }) {
+ const username = route.params.username;
+ console.log('route User Name' + username);
+ console.log(username.replace('@', ''));
+
+ const token = req.cookies.get('token')?.value || '';
+
+ try {
+ const response = await http.get(`/profiles/${username.replace('@', '')}`, {
+ headers: {
+ Authorization: `Token ${token}`,
+ },
+ });
+ console.log('route');
+
+ console.log(response);
+
+ return NextResponse.json({ message: 'Get a Profile Success', response });
+ } catch (err) {
+ return NextResponse.json({ message: 'Get a Profile Fail', err });
+ }
+}
+
+// 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 { GET, 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
new file mode 100644
index 00000000..c5525f61
--- /dev/null
+++ b/app/api/user/route.ts
@@ -0,0 +1,21 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getUserAPI, updateUserAPI } from '@/services/users';
+
+export async function GET(req: NextRequest) {
+ const token = req.cookies.get('token')?.value || '';
+
+ const { user } = await getUserAPI(token);
+
+ return NextResponse.json({
+ message: 'Login successfull',
+ success: true,
+ user,
+ });
+}
+
+export async function PUT(req: NextRequest) {
+ const token = req.cookies.get('token')?.value || '';
+ const user = await req.json();
+
+ return updateUserAPI(user, token);
+}
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/article/[slug]/page.tsx b/app/article/[slug]/page.tsx
index 327832fa..fe854600 100644
--- a/app/article/[slug]/page.tsx
+++ b/app/article/[slug]/page.tsx
@@ -1,55 +1,76 @@
-import CommentCard from '@/components/comments/CommentCard';
-import CommentForm from '@/components/comments/CommentForm';
+'use client';
+import ArticleDeleteButton from '@/components/article/ArticleDeleteButton';
+import ArticleEditButton from '@/components/article/ArticleEditButton';
+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 useUserStore from '@/stores/useUserStore';
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 React from 'react';
+import { container, flex, justifyCenter, paddingTB } from '@/styles/common.css';
+import { User } from '@/types';
+import { useQuery } from '@tanstack/react-query';
+import React, { Suspense } 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 ArticlePage = ({ params: { slug } }: Props) => {
+ const { username } = useUserStore() as User;
+
+ const { data: article } = useQuery({
+ queryKey: ['article', slug],
+ queryFn: async () => await fetch(`/api/articles/${slug}`).then(res => res.json()),
+ select: res => res.data.article,
+ });
+
+ const { title, author, createdAt, body, tagList, favoritesCount } = article;
+
return (
-
- {title}
-
-
-
-
-
-
-
-
{body}
-
-
-
-
-
-
-
-
- {user ? (
-
-
-
-
- ) : (
-
- Sign in or sign up to add comments on this
- article.
-
- )}
+
로딩중...}>
+
+ {title}
+
+
+
+ {author.username === username ? (
+
+ ) : (
+
+
+
+
+ )}
+
+
+
+
로딩중... }>
+
+
{body}
+
+
+
+
+ {author.username === username ? (
+
+ ) : (
+
+
+
+
+ )}
+
-
+
+
);
};
diff --git a/app/editor/[slug]/page.tsx b/app/editor/[slug]/page.tsx
index 5d90da0e..79daed01 100644
--- a/app/editor/[slug]/page.tsx
+++ b/app/editor/[slug]/page.tsx
@@ -1,7 +1,14 @@
-import React from 'react';
-
-const EditorUpdatePage = () => {
- return ;
+'use client';
+import EditForm from '@/components/editor/EditForm';
+type Props = {
+ params: { slug: string };
+};
+const EditorUpdatePage = ({ params: { slug } }: Props) => {
+ return (
+
+ );
};
export default EditorUpdatePage;
diff --git a/app/editor/page.tsx b/app/editor/page.tsx
index cf5adbba..8b7af91a 100644
--- a/app/editor/page.tsx
+++ b/app/editor/page.tsx
@@ -1,66 +1,10 @@
-'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 React, { useState } from 'react';
+import EditForm from '@/components/editor/EditForm';
+import { container } from '@/styles/common.css';
const EditorPage = () => {
- const [formData, setFormData] = useState({
- title: '',
- description: '',
- body: '',
- tagList: [],
- });
-
- const handleSubmit = async (e: any) => {
- e.preventDefault();
- // console.log('들어옴');
-
- // const res = await registerArticle();
- // console.log(res);
- };
-
- const handleChange = (e: any) => {
- setFormData(prev => ({
- ...prev,
- [e.target.name]: e.target.value,
- }));
- };
return (
);
};
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/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/app/login/page.tsx b/app/login/page.tsx
index 718cda6c..54661b13 100644
--- a/app/login/page.tsx
+++ b/app/login/page.tsx
@@ -1,82 +1,12 @@
-'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';
-import { buttonBox } from '@/styles/layout.css';
-import { LoginUser } 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 SignInForm from '@/components/account/SignInForm';
+import { container, flexCenter, flexRow } from '@/styles/common.css';
const LoginPage = () => {
- const router = useRouter();
- const { login } = useUserStore();
-
- const [formData, setFormData] = useState({
- email: '',
- password: '',
- });
-
- const { mutate, isLoading } = useMutation({
- mutationFn: loginAPI,
- onError: error => {
- setFormData({
- email: '',
- password: '',
- });
- alert('아이디 또는 비밀번호가 잘못되었습니다.');
- },
- onSuccess: 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 (
);
};
diff --git a/app/page.tsx b/app/page.tsx
index a792e799..06480169 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,18 +1,23 @@
-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 { container, flex, textCenter } 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 });
+const ArticleList = dynamic(() => import('@/components/article/ArticleList'), { ssr: false });
+const Banner = dynamic(() => import('@/components/layouts/Banner'), { ssr: false });
+
export default function Page() {
return (
- conduit
- A place to share your knowledge.
+
+
conduit
+
A place to share your knowledge.
+
diff --git a/app/register/page.tsx b/app/register/page.tsx
index 8ab918a6..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 } 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();
- 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 (
);
};
diff --git a/app/settings/page.tsx b/app/settings/page.tsx
index ed259661..a1cbed6b 100644
--- a/app/settings/page.tsx
+++ b/app/settings/page.tsx
@@ -1,110 +1,17 @@
-'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 { useMutation } from '@tanstack/react-query';
-import { useState } from 'react';
+import LogoutButton from '@/components/settings/LogoutButton';
+import SettingForm from '@/components/settings/SettingForm';
-const SettingsPage = () => {
- const { email, username, image, bio, password, logout, updateUser } = useUserStore();
-
- // 초기화 함수로 전환
- const [formData, setFormData] = useState({
- image,
- username,
- bio,
- email,
- password,
- });
- const { mutate, isLoading } = useMutation({
- mutationFn: updateUserAPI,
- onError: err => {
- console.error(err);
- },
- onSuccess: res => {
- alert('회원 정보를 변경했습니다!');
- updateUser({
- ...res.user,
- });
- },
- });
-
- const handleSubmit = async (e: any) => {
- e.preventDefault();
+import { container, hr } from '@/styles/common.css';
+import { settingBlock, settingTitle } from '@/styles/settings.css';
- mutate({
- ...formData,
- });
- };
-
- const handleChange = (e: any) => {
- setFormData(prev => ({
- ...prev,
- [e.target.name]: e.target.value,
- }));
- };
+const SettingsPage = () => {
return (
);
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/SignInForm.tsx b/components/account/SignInForm.tsx
new file mode 100644
index 00000000..074143ae
--- /dev/null
+++ b/components/account/SignInForm.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 { LoginUser, UserResponse } from '@/types/api/users';
+import { UserAction } from '@/types/store/userStore';
+import { useRouter } from 'next/navigation';
+import { ChangeEvent, FormEvent, useState } from 'react';
+
+const SignInForm = () => {
+ const router = useRouter();
+ const { saveUserInfo } = useUserStore() as UserAction;
+
+ const [loginUser, setLoginUser] = useState({
+ email: '',
+ password: '',
+ });
+
+ const loginSuccess = (res: UserResponse) => {
+ saveUserInfo({ ...res.user });
+ router.push('/');
+ };
+
+ const loginError = (err: Error) => {
+ console.error(err.message);
+ alert('이메일 또는 비밀번호가 잘못되었습니다.');
+ };
+
+ const { login } = useAuth({ loginSuccess, loginError });
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ login({
+ ...loginUser,
+ });
+ };
+
+ const handleChange = (e: ChangeEvent) => {
+ const { name, value } = e.target;
+ setLoginUser(prev => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+
+ return (
+
+ );
+};
+
+export default SignInForm;
diff --git a/components/account/SignUpForm.tsx b/components/account/SignUpForm.tsx
new file mode 100644
index 00000000..d8adefd0
--- /dev/null
+++ b/components/account/SignUpForm.tsx
@@ -0,0 +1,93 @@
+'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 } from '@/types/api/users';
+import { UserAction } from '@/types/store/userStore';
+
+import { useRouter } from 'next/navigation';
+import { ChangeEvent, FormEvent, useState } from 'react';
+
+const SignUpForm = () => {
+ const router = useRouter();
+ const { saveUserInfo } = useUserStore() as UserAction;
+ const [newUser, setNewUser] = useState({
+ username: '',
+ email: '',
+ password: '',
+ });
+
+ const signupSuccess = (res: any) => {
+ saveUserInfo({
+ ...res.user,
+ });
+ router.push('/');
+ };
+
+ const signupError = () => {
+ alert('회원가입에 실패했습니다.');
+ setNewUser({
+ username: '',
+ email: '',
+ password: '',
+ });
+ };
+
+ const { signup } = useAuth({ signupSuccess, signupError });
+
+ 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/components/article/ArticleDeleteButton.tsx b/components/article/ArticleDeleteButton.tsx
new file mode 100644
index 00000000..9df24e7e
--- /dev/null
+++ b/components/article/ArticleDeleteButton.tsx
@@ -0,0 +1,18 @@
+'use client';
+import Button from '@/composables/Button';
+import { useRouter } from 'next/navigation';
+
+const ArticleDeleteButton = ({ slug }: { slug: string }) => {
+ const router = useRouter();
+ const handleButtonClick = async () => {
+ await fetch(`/api/articles/${slug}`, { method: 'DELETE' }).then(res => res.json());
+ router.push('/');
+ };
+ 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/article/ArticleList.tsx b/components/article/ArticleList.tsx
index 675cf3ed..33c175b0 100644
--- a/components/article/ArticleList.tsx
+++ b/components/article/ArticleList.tsx
@@ -1,44 +1,35 @@
-// 'use client';
-import { ArticleAPI, fetchArticles } from '@/services/articles';
+'use client';
+
import ArticlePreview from './ArticlePreview';
-import { useInfiniteQuery } from '@tanstack/react-query';
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;
+};
+const ArticleList = ({ username }: Props) => {
+ const targetRef = useRef(null);
+ const { tab } = useCurrentTab();
+ const { articlesData } = useArticles({ targetRef, tab, username });
-const ArticleList = async () => {
- const { articles } = await ArticleAPI.all(1);
-
- // 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);
-
- // page.current++;
- // },
- // onError: () => {},
- // });
-
- // return (
- //
- //
- // {data?.pages.map((group, i) => (
- //
- // {group.articles.map(article => (
- //
- // ))}
- //
- // ))}
- //
- // );
-
- return {articles?.map(article =>
)}
;
+ return (
+
+ {articlesData?.pages?.at(0)?.articles?.length === 0 ? (
+ '데이터가 없습니다.'
+ ) : (
+
+ {articlesData?.pages.map((group, i) => (
+
+ {group?.articles?.map((article: Article) =>
)}
+
+ ))}
+
+ )}
+
+
+ );
};
export default ArticleList;
diff --git a/components/article/ArticlePreview.tsx b/components/article/ArticlePreview.tsx
index fe220c37..8e85d993 100644
--- a/components/article/ArticlePreview.tsx
+++ b/components/article/ArticlePreview.tsx
@@ -5,8 +5,10 @@ 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 { useQueryClient } from '@tanstack/react-query';
+import useCurrentTab from '@/stores/useCurrentTab';
+import useArticles from '@/hooks/useArticles';
+
type Props = {
article: any;
};
@@ -14,23 +16,34 @@ const ArticlePreview = ({
article: { title, description, favorited, favoritesCount, tagList, author, createdAt, slug },
}: Props) => {
const router = useRouter();
+ const { tab } = useCurrentTab();
+
+ const queryClient = useQueryClient();
+
+ const onSuccess = () => {
+ queryClient.invalidateQueries({ queryKey: ['articles', tab] });
+ };
+
+ const onError = () => {
+ // 권한이 없을 경우 login 페이지로 이동
+ router.push('/login');
+ };
+
+ const { favorite, unFavorite } = useArticles({ onSuccess, onError });
+
+ const handleButtonClick = (slug: string) => {
+ if (favorited) {
+ unFavorite(slug);
+ } else {
+ favorite(slug);
+ }
+ };
- const { mutate } = useMutation({
- mutationFn: favorited ? unFavoriteArticleAPI : favoriteArticleAPI,
- onError: err => {
- console.error(err);
- },
- onSuccess: res => {
- console.log(res);
-
- console.log('좋아요!');
- },
- });
return (
-