From 942362a03c5e44ee0f04c0ccfe2ec290af5cfa92 Mon Sep 17 00:00:00 2001 From: kimdanran Date: Wed, 17 May 2023 02:18:36 +0900 Subject: [PATCH 1/5] my cute 8 weeks comments' --- src/components/Comment/CommentElement.jsx | 89 ++++++++++++++--- src/components/Comment/index.jsx | 113 ++++++++++++++++++---- src/components/Posts/index.jsx | 17 ++-- src/data/comment.js | 24 +++++ src/data/posts.js | 12 +-- src/index.css | 4 +- src/routes/HomePage.jsx | 1 + src/routes/PostDetailPage.jsx | 3 +- 8 files changed, 215 insertions(+), 48 deletions(-) diff --git a/src/components/Comment/CommentElement.jsx b/src/components/Comment/CommentElement.jsx index 6a8ce7a..7d3c8ee 100644 --- a/src/components/Comment/CommentElement.jsx +++ b/src/components/Comment/CommentElement.jsx @@ -1,29 +1,86 @@ -const CommentElement = (props) => { - // TODO : props 받기 - // TODO : 수정하는 input 내용 관리 +import React from "react"; +import { useEffect, useState } from "react"; +import commentList from "../../data/comment"; +const CommentElement = ({ + id, + post, + created_at, + author, + content, + deleteComment, + editComment, + setEditingCommentId, + setEditingText, + editingCommentId, + editingText, + handleEditText, +}) => { // comment created_at 전처리 - const date = new Date(comment.created_at); + const date = new Date(created_at); const year = date.getFullYear(); let month = date.getMonth() + 1; month = month < 10 ? `0${month}` : month; let day = date.getDate(); day = day < 10 ? `0${day}` : day; -
-
- // 수정중일때와 아닐때를 다르게 보여줘야겠지 - {수정중 ? :

{내용}

} - // 날짜 - - {year}.{month}.{day} - - // 수정, 삭제버튼 -
- // delete 버튼은 수정이 아닐때만 보이게 해줘 + const handleEditClick = () => { + setEditingCommentId(id); + setEditingText(content); + }; + + const handleEditSubmit = () => { + editComment(id); + }; + + return ( +
+
+
+
+ {editingCommentId === id ? ( + <> +
+ + +
+ + ) : ( + <> +

{content}

+ + {year}.{month}.{day} + + + )} +
+
+ + {editingCommentId !== id && ( + + )} +
+
{" "}
-
; + ); }; export default CommentElement; diff --git a/src/components/Comment/index.jsx b/src/components/Comment/index.jsx index e4d260b..6d0bd62 100644 --- a/src/components/Comment/index.jsx +++ b/src/components/Comment/index.jsx @@ -1,26 +1,105 @@ -import { useState } from "react"; -import comments from "../../data/comments"; +import { useEffect, useState } from "react"; import CommentElement from "./CommentElement.jsx"; +import commentList from "../../data/comment.js"; const Comment = () => { - // TODO 1: comments 불러와서 저장해야겟즤 + const [commentListinPost, setCommentListinPost] = useState(commentList); + const [commentInputValue, setCommentInputValue] = useState({ content: "" }); + const [editingText, setEditingText] = useState(""); + const [editingCommentId, setEditingCommentId] = useState(null); - // TODO 2: comment추가하는 input 관리해줘야겟지 + // show comment in commentList + useEffect(() => { + setCommentListinPost(commentList); + }, []); - // TODO 3: comment Form 제출됐을때 실행되는 함수 만들어줘 + // handle comment input value + const handleCommentData = (e) => { + const { id, value } = e.target; + setCommentInputValue({ ...commentInputValue, [id]: value }); + }; - // TODO 4: commet Delete 하는 함수 만들어죠 - return ( -
-

Comments

- // commentElement - // 가 comment마다 반복시켜야즤 + // add comment + const addComment = (e) => { + e.preventDefault(); + const newComment = { + id: commentListinPost.length + 1, + post: "post", + created_at: new Date(), + author: { + id: 1, + username: "베이비", + }, + content: commentInputValue.content, + }; + setCommentListinPost([...commentListinPost, newComment]); + setCommentInputValue({ content: "" }); + }; -
- // TODO 2-3 : comment 추가하는 comment form 만들어주기 -
-
- ); + //deleteComment + const deleteComment = (id) => { + const updatedCommentList = commentListinPost.filter( + (comment) => comment.id !== id + ); + setCommentListinPost(updatedCommentList); + }; + + //edit Comment + const editComment = (id) => { + const updatedCommentList = commentListinPost.map((comment) => { + if (comment.id === id) { + return { ...comment, content: editingText }; + } + return comment; + }); + setCommentListinPost(updatedCommentList); + setEditingCommentId(null); + setEditingText(""); + }; + + const handleEditText = (e) => { + setEditingText(e.target.value); + }; + + return ( +
+

댓글💬

+
+ {commentListinPost.map((comment) => ( + + ))} +
+
+ + +
+
+ ); }; -export default Comment; \ No newline at end of file +export default Comment; diff --git a/src/components/Posts/index.jsx b/src/components/Posts/index.jsx index 760e0a6..29e2ff9 100644 --- a/src/components/Posts/index.jsx +++ b/src/components/Posts/index.jsx @@ -1,7 +1,6 @@ import { useEffect } from "react"; import { Link } from "react-router-dom"; - export const SmallPost = ({ post, toggleSearchTagId, searchTagIdList }) => { const onClickLike = () => { console.log("나도 좋아!"); @@ -47,11 +46,17 @@ export const BigPost = ({ post }) => { }; return ( -
+
-

{post.title}

- {post.author.username} -
{post.content}
+

+ {post.title} +

+ + 작성자 : {post.author.username} + +
+ {post.content} +
{post.tags && post.tags.map((tag) => ( @@ -61,7 +66,7 @@ export const BigPost = ({ post }) => { ))}
- {post.like_users.length > 0 && `❤️ ${post.like_users.length}`} + {post.like_users.length > 0 && ` 🦁 ${post.like_users.length}`}
diff --git a/src/data/comment.js b/src/data/comment.js index e69de29..ce98f54 100644 --- a/src/data/comment.js +++ b/src/data/comment.js @@ -0,0 +1,24 @@ +const commentList = [ + { + id: 1, + content: "선착순 30명이면 다 데려가겠다는거 아닙니까?", + created_at: "2023-04-18T15:09:43Z", + post: 1, + author: { + id: 2, + username: "user2", + }, + }, + { + id: 2, + content: "너 누구야ㅡㅡ 진모지?", + created_at: "2023-04-18T15:09:43Z", + post: 1, + author: { + id: 2, + username: "user2", + }, + }, +]; + +export default commentList; diff --git a/src/data/posts.js b/src/data/posts.js index 10e3f15..a2eff43 100644 --- a/src/data/posts.js +++ b/src/data/posts.js @@ -1,15 +1,15 @@ const posts = [ { id: 1, - title: "#1 post", - content: "My Number one post content", + title: "오늘 한강 갈 사람?😘", + content: "오늘 날씨도 좋은데 한강 가서 치맥하실분? 선착순 30명 받는다.", author: { id: 1, username: "지현" }, tags: [ - { id: 1, content: "멋사" }, - { id: 2, content: "개발자" }, - { id: 3, content: "서울대" }, + { id: 1, content: "한강" }, + { id: 2, content: "맥주" }, + { id: 3, content: "뽀로로" }, ], - like_users: [1, 2], + like_users: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], created_at: "2023-02-04T07:42:50.658501Z", }, { diff --git a/src/index.css b/src/index.css index 4291fd1..f1c1af9 100644 --- a/src/index.css +++ b/src/index.css @@ -11,7 +11,7 @@ } /* 모든 버튼 일괄 적용 */ .button { - @apply bg-orange-400 text-white font-medium hover:text-black rounded-xl text-lg p-3.5; + @apply bg-transparent text-white font-medium hover:text-black rounded-md text-lg p-3.5; } /* 모든 form 일괄 적용 */ .form { @@ -22,7 +22,7 @@ } /* 모든 input 일괄 적용 */ .input { - @apply border-[1px] border-white w-full px-6 py-3 rounded-md text-white bg-transparent placeholder-opacity-50 focus:outline-none focus:ring-[1px] focus:ring-orange-400 focus:border-transparent; + @apply border-[1px] border-white w-full px-6 py-3 rounded-md text-white bg-transparent placeholder-opacity-50 focus:outline-none focus:ring-[1px] focus:ring-lime-400 focus:border-transparent; } body { diff --git a/src/routes/HomePage.jsx b/src/routes/HomePage.jsx index 6bec633..111026f 100644 --- a/src/routes/HomePage.jsx +++ b/src/routes/HomePage.jsx @@ -121,3 +121,4 @@ const HomePage = () => { }; export default HomePage; + diff --git a/src/routes/PostDetailPage.jsx b/src/routes/PostDetailPage.jsx index 02e4269..244219c 100644 --- a/src/routes/PostDetailPage.jsx +++ b/src/routes/PostDetailPage.jsx @@ -3,6 +3,7 @@ import { useParams } from "react-router-dom"; import { BigPost } from "../components/Posts"; import { Link } from "react-router-dom"; import posts from "../data/posts"; +import Comment from "../components/Comment"; const PostDetailPage = () => { // parameter로 받은 id에 해당하는 post를 찾아서 넣자 @@ -28,7 +29,7 @@ const PostDetailPage = () => {
{/* post detail component */} - +
From bb5f3d402fb417fb17bc769e2109b6c8f7e8f186 Mon Sep 17 00:00:00 2001 From: kimdanran Date: Fri, 26 May 2023 00:55:50 +0900 Subject: [PATCH 2/5] Make Like button and Delete function --- package-lock.json | 169 ++++++++++++++++++++++ package.json | 2 + src/apis/api.js | 157 ++++++++++++++++++++ src/apis/axios.js | 58 ++++++++ src/components/Comment/CommentElement.jsx | 165 ++++++++++++++++++--- src/components/Comment/index.jsx | 51 ++++--- src/components/Form/index.jsx | 56 ++++--- src/components/Header/index.jsx | 37 ++++- src/components/Posts/index.jsx | 94 ++++++++++-- src/routes/HomePage.jsx | 114 ++++++++------- src/routes/PostCreatePage.jsx | 58 +++----- src/routes/PostDetailPage.jsx | 87 +++++++---- src/routes/PostEditPage.jsx | 78 +++++++++- src/routes/SignInPage.jsx | 16 +- src/routes/SignUpPage.jsx | 19 ++- src/utils/cookie.js | 21 +++ 16 files changed, 971 insertions(+), 211 deletions(-) create mode 100644 src/apis/api.js create mode 100644 src/apis/axios.js create mode 100644 src/utils/cookie.js diff --git a/package-lock.json b/package-lock.json index 9b424cf..3c92c77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.4.0", "react": "^18.2.0", + "react-cookie": "^4.1.1", "react-dom": "^18.2.0", "react-router-dom": "^6.11.1", "react-scripts": "5.0.1", @@ -3534,6 +3536,11 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + }, "node_modules/@types/eslint": { "version": "8.37.0", "license": "MIT", @@ -3581,6 +3588,15 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "license": "MIT" @@ -4683,6 +4699,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "3.1.1", "license": "Apache-2.0", @@ -7963,6 +8002,19 @@ "he": "bin/he" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/hoopy": { "version": "0.1.4", "license": "MIT", @@ -12879,6 +12931,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "license": "MIT" @@ -13009,6 +13066,19 @@ "node": ">=14" } }, + "node_modules/react-cookie": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz", + "integrity": "sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.0.1", + "hoist-non-react-statics": "^3.0.0", + "universal-cookie": "^4.0.0" + }, + "peerDependencies": { + "react": ">= 16.3.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "license": "MIT", @@ -14865,6 +14935,23 @@ "node": ">=8" } }, + "node_modules/universal-cookie": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", + "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", + "dependencies": { + "@types/cookie": "^0.3.3", + "cookie": "^0.4.0" + } + }, + "node_modules/universal-cookie/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/universalify": { "version": "2.0.0", "license": "MIT", @@ -17847,6 +17934,11 @@ "@types/node": "*" } }, + "@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + }, "@types/eslint": { "version": "8.37.0", "requires": { @@ -17888,6 +17980,15 @@ "@types/node": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "6.1.0" }, @@ -18586,6 +18687,28 @@ "axe-core": { "version": "4.7.0" }, + "axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "axobject-query": { "version": "3.1.1", "requires": { @@ -20566,6 +20689,21 @@ "he": { "version": "1.2.0" }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, "hoopy": { "version": "0.1.4" }, @@ -23346,6 +23484,11 @@ } } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "psl": { "version": "1.9.0" }, @@ -23419,6 +23562,16 @@ "whatwg-fetch": "^3.6.2" } }, + "react-cookie": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz", + "integrity": "sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==", + "requires": { + "@types/hoist-non-react-statics": "^3.0.1", + "hoist-non-react-statics": "^3.0.0", + "universal-cookie": "^4.0.0" + } + }, "react-dev-utils": { "version": "12.0.1", "requires": { @@ -24557,6 +24710,22 @@ "crypto-random-string": "^2.0.0" } }, + "universal-cookie": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", + "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", + "requires": { + "@types/cookie": "^0.3.3", + "cookie": "^0.4.0" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + } + } + }, "universalify": { "version": "2.0.0" }, diff --git a/package.json b/package.json index ac7db0d..d3fe664 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.4.0", "react": "^18.2.0", + "react-cookie": "^4.1.1", "react-dom": "^18.2.0", "react-router-dom": "^6.11.1", "react-scripts": "5.0.1", diff --git a/src/apis/api.js b/src/apis/api.js new file mode 100644 index 0000000..ef62479 --- /dev/null +++ b/src/apis/api.js @@ -0,0 +1,157 @@ +import { instance, instanceWithToken } from "./axios"; + +//백엔드의 명세를 잘 봐야한다. IF else 처리할 때 상태 처리 +// src/api/api.js +// Account 관련 API들 + +//signin +export const signIn = async (data) => { + const response = await instance.post("/account/signin/", data); + if (response.status === 200) { + window.location.href = "/"; + } else { + console.log("Error"); + } +}; + +//signup +export const signUp = async (data) => { + const response = await instance.post("/account/signup/", data); + if (response.status === 200) { + window.location.href = "/"; + } + return response; +}; + +//post 관련 api들 + +//전부 가져오기 +export const getPosts = async () => { + const response = await instance.get("/post/"); + return response.data; +}; + +//하나만 가져오기 +export const getPost = async (id) => { + const response = await instance.get(`/post/${id}/`); + return response.data; +}; + +// create post +export const createPost = async (data, navigate) => { + const response = await instanceWithToken.post("/post/", data); + if (response.status === 201) { + console.log("POST SUCCESS"); + navigate("/"); + } else { + console.log("[ERROR] error while creating post"); + } +}; + +//update post +export const updatePost = async (id, data, navigate) => { + const response = await instanceWithToken.patch(`/post/${id}/`, data); + if (response.status === 200) { + console.log("POST UPDATE SUCCESS"); + navigate(-1); + } else { + console.log("[ERROR] error while updating post"); + } +}; + +//----과제---- + +export const deletePost = async (id, navigate) => { + const response = await instanceWithToken.delete(`/post/${id}/`); + if (response.status === 200) { + console.log("POST DELETE SUCCESS"); + navigate(-1); + } else { + console.log("[ERROR] error while deleting post"); + } +}; + +// 과제!! +export const getLikePost = async (postId) => { + try { + const response = await instanceWithToken.post(`/post/${postId}/like/`); + if (response.status === 200) { + console.log("LIKE SUCCESS"); + return response.data; // Return updated post data + } else { + console.log("[ERROR] Error while like post"); + return null; // Indicate unsuccessful like + } + } catch (error) { + console.error("[ERROR] Error while like post:", error); + throw error; + } +}; + +// Tag 관련 API들 +//전체태그 가져오기 +export const getTags = async () => { + const response = await instance.get("/tag/"); + return response.data; +}; + +//태그 만들기 +export const createTag = async (data) => { + const response = await instanceWithToken.post("/tag/", data); + if (response.status === 201) { + console.log("TAG SUCCESS"); + } else { + console.log("[ERROR] error while creating tag"); + } + return response; // response 받아서 그 다음 처리 +}; + +// Comment 관련 API들 +//모든 코멘트 가져오기 +export const getComments = async (postId) => { + const response = await instance.get(`/comment/?post=${postId}`); + return response.data; +}; + +//코멘트 만들기 +export const createComment = async (data) => { + const response = await instanceWithToken.post("/comment/", data); + if (response.status === 201) { + console.log("COMMENT SUCCESS"); + window.location.reload(); // 새로운 코멘트 생성시 새로고침으로 반영 + } else { + console.log("[ERROR] error while creating comment"); + } +}; + +//코멘트 update +export const updateComment = async (id, data) => { + const response = await instanceWithToken.patch(`/comment/${id}/`, data); + if (response.status === 200) { + console.log("COMMENT UPDATE SUCCESS"); + window.location.reload(); + } else { + console.log("[ERROR] error while updating comment"); + } +}; + +// ---------과제 !! +export const deleteComment = async (id) => { + const response = await instanceWithToken.delete(`/comment/${id}/`); + if (response.status === 200) { + console.log("COMMENT DELETE SUCCESS"); + window.location.reload(); + } else { + console.log("[ERROR] error while deleting comment"); + } +}; + +export const getUser = async () => { + const response = await instanceWithToken.get("/account/info/"); + if (response.status === 200) { + console.log("GET USER SUCCESS"); + } else { + console.log("[ERROR] error while updating comment"); + } + return response.data; +}; diff --git a/src/apis/axios.js b/src/apis/axios.js new file mode 100644 index 0000000..bbd4918 --- /dev/null +++ b/src/apis/axios.js @@ -0,0 +1,58 @@ +// src/apis/axios.js + +import axios from "axios"; +import { getCookie } from "../utils/cookie"; + +// baseURL, credential, 헤더 세팅 +axios.defaults.baseURL = 'http://localhost:8000/api'; +axios.defaults.withCredentials = true; +// 쿠키 사용해라 +axios.defaults.headers.post['Content-Type'] = 'application/json'; +//post 일때는 이렇게 해라 +axios.defaults.headers.common['X-CSRFToken'] = getCookie('csrftoken'); +//공통적으로 쿠키에서 담아와라 + + +// 누구나 접근 가능한 API들 +export const instance = axios.create(); + +// Token 있어야 접근 가능한 API들 (로그인 등등..) - 얘는 토큰을 넣어줘야 해요 +export const instanceWithToken = axios.create(); + +// instanceWithToken에는 쿠키에서 토큰을 찾고 담아줍시다! +instanceWithToken.interceptors.request.use( + // 요청을 보내기전 수행할 일 + // 사실상 이번 세미나에 사용할 부분은 이거밖에 없어요 + (config) => { + const accessToken = getCookie('access_token'); + + if (!accessToken) { + // token 없으면 리턴 + return; + } else { + // token 있으면 헤더에 담아주기 (Authorization은 장고에서 JWT 토큰을 인식하는 헤더 key) + config.headers["Authorization"] = `Bearer ${accessToken}`; + } + return config; + }, + + // 클라이언트 요청 오류 났을 때 처리 + (error) => { + // 콘솔에 찍어주고, 요청을 보내지 않고 오류를 발생시킴 + console.log("Request Error!!"); + return Promise.reject(error); + } +); + +instanceWithToken.interceptors.response.use( + (response) => { + // 서버 응답 데이터를 프론트에 넘겨주기 전 수행할 일 + console.log("Interceptor Response!!"); + return response; + }, + (error) => { + // 서버가 오류를 응답했을 때 처리 - 콘솔 찍어주고, 프론트에게 보내지 않고 오류를 발생시킴 + console.log("Response Error!!"); + return Promise.reject(error); + } +); \ No newline at end of file diff --git a/src/components/Comment/CommentElement.jsx b/src/components/Comment/CommentElement.jsx index 7d3c8ee..dd2cdc5 100644 --- a/src/components/Comment/CommentElement.jsx +++ b/src/components/Comment/CommentElement.jsx @@ -1,6 +1,112 @@ -import React from "react"; -import { useEffect, useState } from "react"; -import commentList from "../../data/comment"; +// import React from "react"; +// import { useEffect, useState } from "react"; +// import commentList from "../../data/comment"; +// import { getUser } from "../../apis/api"; +// import { getCookie } from "../../utils/cookie"; + +// const CommentElement = ({ +// id, +// post, +// created_at, +// author, +// content, +// deleteComment, +// editComment, +// setEditingCommentId, +// setEditingText, +// editingCommentId, +// editingText, +// handleEditText, +// }) => { +// const [user, setUser] = useState(null); +// const [isEdit, setIsEdit] = useState(false); + +// useEffect(() => { +// // access_token이 있으면 유저 정보 가져옴 +// if (getCookie("access_token")) { +// const getUserAPI = async () => { +// const user = await getUser(); +// setUser(user); +// }; +// getUserAPI(); +// } +// }, []); + +// // comment created_at 전처리 +// const date = new Date(created_at); +// const year = date.getFullYear(); +// let month = date.getMonth() + 1; +// month = month < 10 ? `0${month}` : month; +// let day = date.getDate(); +// day = day < 10 ? `0${day}` : day; + +// const handleEditClick = () => { +// setEditingCommentId(id); +// setEditingText(content); +// }; + +// const handleEditSubmit = () => { +// editComment(id); +// }; + +// return ( +//
+//
+//
+//
+// {editingCommentId === id ? ( +// <> +//
+// +// +//
+// +// ) : ( +// <> +//

{content}

+// +// {year}.{month}.{day} +// +// +// )} +//
+// {user?.id === author ? ( +//
+// {!isEdit && ( +// +// )} +// {editingCommentId !== id && ( +// +// )} +//
+//
{" "} +//
+//
+// ); +// }; + +// export default CommentElement; + +import React, { useEffect, useState } from "react"; +import { getUser, updateComment } from "../../apis/api"; +import { getCookie } from "../../utils/cookie"; const CommentElement = ({ id, @@ -16,6 +122,20 @@ const CommentElement = ({ editingText, handleEditText, }) => { + const [user, setUser] = useState(null); + const [isEdit, setIsEdit] = useState(false); + + useEffect(() => { + // access_token이 있으면 유저 정보 가져옴 + if (getCookie("access_token")) { + const getUserAPI = async () => { + const user = await getUser(); + setUser(user); + }; + getUserAPI(); + } + }, []); + // comment created_at 전처리 const date = new Date(created_at); const year = date.getFullYear(); @@ -29,8 +149,11 @@ const CommentElement = ({ setEditingText(content); }; - const handleEditSubmit = () => { - editComment(id); + const handleEditSubmit = async () => { + if (user && user.id === author) { + await updateComment(id, { content: editingText }); + editComment(id); + } }; return ( @@ -64,20 +187,24 @@ const CommentElement = ({ )}
-
- - {editingCommentId !== id && ( - - )} -
-
{" "} + {user?.id === author ? ( +
+ {!isEdit && ( + + )} + {editingCommentId !== id && ( + + )} +
+ ) : null} +
); diff --git a/src/components/Comment/index.jsx b/src/components/Comment/index.jsx index 6d0bd62..21c1a73 100644 --- a/src/components/Comment/index.jsx +++ b/src/components/Comment/index.jsx @@ -1,47 +1,50 @@ import { useEffect, useState } from "react"; import CommentElement from "./CommentElement.jsx"; -import commentList from "../../data/comment.js"; +import { getComments } from "../../apis/api.js"; +import { createComment } from "../../apis/api.js"; -const Comment = () => { - const [commentListinPost, setCommentListinPost] = useState(commentList); - const [commentInputValue, setCommentInputValue] = useState({ content: "" }); +const Comment = ({ postId }) => { + const [commentInputValue, setCommentInputValue] = useState(""); const [editingText, setEditingText] = useState(""); const [editingCommentId, setEditingCommentId] = useState(null); + const [commentListinPost, setCommentListinPost] = useState([]); + + useEffect(() => { + const getCommentsAPI = async () => { + const comments = await getComments(postId); + setCommentListinPost(comments); + }; + getCommentsAPI(); + }, [postId]); + //getComments를 이용해 모든 comment 가져오고 commentList에 담아두기 // show comment in commentList useEffect(() => { - setCommentListinPost(commentList); + setCommentListinPost(commentListinPost); }, []); // handle comment input value const handleCommentData = (e) => { const { id, value } = e.target; - setCommentInputValue({ ...commentInputValue, [id]: value }); + setCommentInputValue(value); }; // add comment const addComment = (e) => { e.preventDefault(); - const newComment = { - id: commentListinPost.length + 1, - post: "post", - created_at: new Date(), - author: { - id: 1, - username: "베이비", - }, - content: commentInputValue.content, - }; - setCommentListinPost([...commentListinPost, newComment]); - setCommentInputValue({ content: "" }); + createComment({ post: postId, content: commentInputValue }); + setCommentInputValue(""); }; //deleteComment const deleteComment = (id) => { - const updatedCommentList = commentListinPost.filter( - (comment) => comment.id !== id - ); - setCommentListinPost(updatedCommentList); + const confirmed = window.confirm("삭제할꼬얌?"); + if (confirmed) { + const updatedCommentList = commentListinPost.filter( + (comment) => comment.id !== id + ); + setCommentListinPost(updatedCommentList); + } }; //edit Comment @@ -71,7 +74,7 @@ const Comment = () => { id={comment.id} post={comment.post} created_at={comment.created_at} - author={comment.author} + author={comment.author.id} content={comment.content} commentListinPost={commentListinPost} setCommentListinPost={setCommentListinPost} @@ -90,7 +93,7 @@ const Comment = () => { type="text" placeholder="댓글을 입력해주세요" id="content" - value={commentInputValue["content"]} + value={commentInputValue} onChange={handleCommentData} className="input h-14" /> diff --git a/src/components/Form/index.jsx b/src/components/Form/index.jsx index e729284..e202838 100644 --- a/src/components/Form/index.jsx +++ b/src/components/Form/index.jsx @@ -122,7 +122,14 @@ export const SignInForm = ({ formData, setFormData, handleSignInSubmit }) => { ); }; -export const PostForm = ({ onSubmit, tags, formData, setFormData }) => { +export const PostForm = ({ + onSubmit, + tags, + formData, + setFormData, + setTags, + handleCreateTag, +}) => { //태그 Input 안에 값 const [tagInputValue, setTagInputValue] = useState(""); @@ -134,37 +141,43 @@ export const PostForm = ({ onSubmit, tags, formData, setFormData }) => { }; //태그 인풋 값 바뀌면 그에 따라서 자동 완성값들도 변경 + const handleTag = (e) => { setTagInputValue(e.target.value); if (e.target.value) { - const autoCompleteData = tags.filter((tag) => - tag.includes(e.target.value) + const autoCompleteData = tags.filter( + (tag) => typeof tag === "string" && tag.includes(e.target.value) ); setAutoCompletes(autoCompleteData); } }; - // 자동완성 값이 있는 버튼을 눌렀을 때 이를 태그에 등록 + // 자동성 값이 있는 버튼을 눌렀을 때 이를 태그에 등록 const handleAutoCompletes = (autoComplete) => { - const selectedTag = tags.find((tag) => tag === autoComplete); - - if (formData.tags.includes(selectedTag)) return; + if (formData.tags.includes(autoComplete)) return; setFormData({ ...formData, - tags: [...formData.tags, selectedTag], + tags: [...formData.tags, autoComplete], }); setTagInputValue(""); setAutoCompletes([]); }; - // 추가 버튼 혹인 엔터 누르면 태그 생성 const addTag = (e) => { e.preventDefault(); + if (!Array.isArray(formData.tags)) return; - // 입력한 내용이 이미 등록된 태그면 그냥 등록 안됨 - if (formData.tags.find((tag) => tag === tagInputValue)) return; + // Check if formData.tags is defined and an array before using the find method + if ( + Array.isArray(formData.tags) && + formData.tags.find((tag) => tag === tagInputValue) + ) { + return; + } + const newTag = { id: Date.now().toString(), content: tagInputValue }; // Create a new tag object + setTags([...tags, newTag]); // Add the new tag to the tags state setFormData({ ...formData, tags: [...formData.tags, tagInputValue], @@ -174,12 +187,11 @@ export const PostForm = ({ onSubmit, tags, formData, setFormData }) => { setAutoCompletes([]); }; - // X버튼 눌렀을때 태그 삭제 const deleteTag = (tag) => { - setFormData({ - ...formData, - tags: formData.tags.filter((t) => t !== tag), - }); + setFormData((prevFormData) => ({ + ...prevFormData, + tags: prevFormData.tags.filter((t) => t !== tag), + })); }; return ( @@ -239,18 +251,18 @@ export const PostForm = ({ onSubmit, tags, formData, setFormData }) => { ))} - {formData.tags && ( + {tags && (
- {formData.tags.map((tag) => ( + {tags.map((tag) => (
-

#{tag}

+

#{tag.content}

{/* 삭제버튼 */} -
))} diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index 65b78a2..60a95a0 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -1,7 +1,22 @@ import lion from "../../assets/images/lion.jpeg"; import { Link } from "react-router-dom"; +import { getCookie, removeCookie } from "../../utils/cookie"; +import { useEffect, useState } from "react"; const Header = () => { + const [isLoggedIn, setIsLoggedIn] = useState(""); + + useEffect(() => { + const loggedIn = getCookie("access_token") ? true : false; + setIsLoggedIn(loggedIn); + }, []); + + const handleLogout = () => { + removeCookie("access_token"); + removeCookie("refresh_token"); + window.location.href = "/"; + }; + return (
{
- - sign in - - - sign up - + {!isLoggedIn ? ( + <> + + sign In + + + sign up + + + ) : ( + <> + + log out + + + )}
); diff --git a/src/components/Posts/index.jsx b/src/components/Posts/index.jsx index 29e2ff9..0c99e1c 100644 --- a/src/components/Posts/index.jsx +++ b/src/components/Posts/index.jsx @@ -1,10 +1,46 @@ import { useEffect } from "react"; import { Link } from "react-router-dom"; +import { getUser } from "../../apis/api"; +import { getCookie } from "../../utils/cookie"; +import { useState } from "react"; +import { getLikePost } from "../../apis/api"; +import { useParams } from "react-router-dom"; + +// add api call for liking post export const SmallPost = ({ post, toggleSearchTagId, searchTagIdList }) => { - const onClickLike = () => { - console.log("나도 좋아!"); - // add api call for liking post here + const [user, setUser] = useState(); + const [likeCount, setLikeCount] = useState(post.like_users.length); + const [liked, setLiked] = useState(false); + + useEffect(() => { + if (getCookie("access_token")) { + const getUserAPI = async () => { + const user = await getUser(); + setUser(user); + }; + getUserAPI(); + } + }, []); + + const onClickLike = async () => { + if (user) { + try { + const response = await getLikePost(post.id); + console.log( + "🚀 ~ file: index.jsx:30 ~ onClickLike ~ response:", + response + ); + if (response) { + setLiked(response.liked); + setLikeCount(response.like_users.length); + } else { + console.log("[ERROR] Error while liking post"); + } + } catch (error) { + console.error("Error liking post:", error); + } + } }; return ( @@ -12,7 +48,7 @@ export const SmallPost = ({ post, toggleSearchTagId, searchTagIdList }) => {

{post.title}

{post.author.username}

- {post.tags.map((tag) => { + {post.tags?.map((tag) => { const tagClassname = searchTagIdList.includes(tag.id) ? "active" : ""; return (
-
- {post.like_users.length > 0 && `❤️ ${post.like_users.length}`} +
+ {liked ? "🦁 이미 좋아요를 눌렀어요" : "🦁 좋아요"} {likeCount}
@@ -40,9 +79,38 @@ export const SmallPost = ({ post, toggleSearchTagId, searchTagIdList }) => { }; export const BigPost = ({ post }) => { - const onClickLike = () => { - console.log("나도 좋아!"); - // add api call for liking post here + const [user, setUser] = useState(); + const [likeCount, setLikeCount] = useState(post?.like_users?.length); + const [liked, setLiked] = useState(false); + + useEffect(() => { + if (getCookie("access_token")) { + const getUserAPI = async () => { + const user = await getUser(); + setUser(user); + }; + getUserAPI(); + } + }, []); + + const onClickLike = async () => { + if (user) { + try { + const response = await getLikePost(post.id); + console.log( + "🚀 ~ file: index.jsx:30 ~ onClickLike ~ response:", + response + ); + if (response) { + setLiked(response.liked); + setLikeCount(response.like_users.length); + } else { + console.log("[ERROR] Error while liking post"); + } + } catch (error) { + console.error("Error liking post:", error); + } + } }; return ( @@ -52,21 +120,21 @@ export const BigPost = ({ post }) => { {post.title} - 작성자 : {post.author.username} + 작성자 : {post?.author?.username}
{post.content}
{post.tags && - post.tags.map((tag) => ( + post.tags?.map((tag) => ( #{tag.content} ))}
-
- {post.like_users.length > 0 && ` 🦁 ${post.like_users.length}`} +
+ {liked ? "🦁 이미 좋아요를 눌렀어요" : "🦁 좋아요"} {likeCount}
diff --git a/src/routes/HomePage.jsx b/src/routes/HomePage.jsx index 111026f..b11359f 100644 --- a/src/routes/HomePage.jsx +++ b/src/routes/HomePage.jsx @@ -2,73 +2,85 @@ import { useState, useEffect } from "react"; import { SmallPost } from "../components/Posts"; import postsData from "../data/posts"; import { Link } from "react-router-dom"; +import { getPosts } from "../apis/api"; +import { getTags } from "../apis/api"; +import { getCookie } from "../utils/cookie"; +// import axios from "axios"; const HomePage = () => { - const [postList, setPostList] = useState(postsData); - const [tags, setTags] = useState(new Map()); + //원래는 const [posts, setPosts] = useState(postData); 를 했었는데 아래 빈 어레이.. + const [postList, setPostList] = useState([]); + const [tags, setTags] = useState([]); const [searchTags, setSearchTags] = useState([]); const [searchTagIdList, setSearchTagIdList] = useState([]); + //추가 + useEffect(() => { - const tagList = postsData.reduce((acc, post) => { - for (let tag of post.tags) { - if (!acc.has(tag.id)) { - acc.set(tag.id, tag.content); - } else { - continue; - } - } - return acc; - }, new Map()); - setTags(tagList); - setSearchTags(Array.from(tagList.keys())); + const getPostsAPI = async () => { + const posts = await getPosts(); + setPostList(posts); + }; + getPostsAPI(); + }, []); + + useEffect(() => { + const getPostsAPI = async () => { + const posts = await getPosts(); + setPostList(posts); + }; + getPostsAPI(); + + //추가 + const getTagsAPI = async () => { + const tags = await getTags(); + const tagContents = tags?.map((tag) => { + return { id: tag.id, content: tag.content }; + }); + setTags(tagContents); + setSearchTags(tagContents); + }; + console.log(getTagsAPI); + getTagsAPI(); + // getTags() 이용해서 tag들 불러오고 tags?.map을 이용해서 tagContents에 + // tag.content만 저장한 후, tags와 searchTags에 저장 }, []); + console.log(tags); + const handleChange = (e) => { const { value } = e.target; - // [key, value] - const newTags = tags.entries().filter((tag) => { - return tag[1].includes(value); + const newTags = tags.filter((tag) => { + return tag.content.includes(value); }); setSearchTags(newTags); + console.log(newTags); }; useEffect(() => { - if (searchTagIdList.length == 0) { - console.log( - "🚀 ~ file: Home.jsx:37 ~ useEffect ~ searchTagIdList:", - searchTagIdList - ); - setPostList(postsData); + if (searchTagIdList.length === 0) { + setPostList([]); } else { - const newPostList = postsData.filter((post) => { - return post.tags.some((tag) => searchTagIdList.includes(tag.id)); - }); + const newPostList = postList.filter((post) => + post.tags.some((tag) => searchTagIdList.includes(tag.id)) + ); setPostList(newPostList); } }, [searchTagIdList]); + + const toggleSearchTagId = (id) => { if (searchTagIdList.includes(id)) { - const newSearchTagIdList = searchTagIdList.filter((tagId) => { - return tagId !== id; - }); - + const newSearchTagIdList = searchTagIdList.filter( + (tagId) => tagId !== id + ); setSearchTagIdList(newSearchTagIdList); } else { setSearchTagIdList([...searchTagIdList, id]); } }; - // const handleTagFilter = (e) => { - // const { innerText } = e.target; - // setSearchValue(innerText.slice(1)); - // const newPostList = posts.filter((post) => { - // return post.tags.some((tag) => tag.content === innerText.slice(1)); - // }); - // setPostList(newPostList); - // }; - return (
@@ -84,17 +96,17 @@ const HomePage = () => { className="border border-orange-400 outline-none rounded-2xl text-center py-2 px-20 text-orange-400 bg-transparent" />
- {searchTags.map((id) => { - const searchTagClassname = searchTagIdList.includes(id) + {searchTags.map((tag, index) => { + const searchTagClassname = searchTagIdList.includes(tag.id) ? "tag active mr-2" : "tag mr-2"; return ( ); })} @@ -111,14 +123,16 @@ const HomePage = () => { /> ))}
-
- - Post - -
+ + {getCookie("access_token") ? ( +
+ + Post + +
+ ) : null}
); }; export default HomePage; - diff --git a/src/routes/PostCreatePage.jsx b/src/routes/PostCreatePage.jsx index 54c6f04..6d75382 100644 --- a/src/routes/PostCreatePage.jsx +++ b/src/routes/PostCreatePage.jsx @@ -1,58 +1,44 @@ import { useEffect, useState } from "react"; -import { BigPost } from "../components/Posts"; -import posts from "../data/posts"; import { PostForm } from "../components/Form"; +import { useNavigate } from "react-router-dom"; +import { createPost, getTags } from "../apis/api"; const PostCreatePage = () => { - const [isSubmitted, setIsSubmitted] = useState(false); - // 화면그리기 const [formData, setFormData] = useState({ - id: posts.length, title: "", content: "", - author: { id: posts.length, username: "베이비" }, tags: [], }); const [tags, setTags] = useState([]); useEffect(() => { - const duplicatedTagList = posts.reduce((acc, post) => { - for (let tag of post.tags) { - acc.add(tag.content); - } - - return acc; - }, new Set()); - - const tagList = [...duplicatedTagList]; - - setTags([...tagList]); + const getTagsAPI = async () => { + const tags = await getTags(); + const tagContents = tags?.map((tag) => { + return tag.content; + }); + setTags(tagContents); + }; + getTagsAPI(); }, []); + const navigate = useNavigate(); + const onSubmit = (e) => { - //TODO : api connect e.preventDefault(); - console.log(formData); + createPost(formData, navigate); }; return ( - <> - {isSubmitted ? ( -
- -
- ) : ( -
-

New Post

- -
- )} - +
+

New Post

+ +
); }; diff --git a/src/routes/PostDetailPage.jsx b/src/routes/PostDetailPage.jsx index 244219c..16e0840 100644 --- a/src/routes/PostDetailPage.jsx +++ b/src/routes/PostDetailPage.jsx @@ -4,42 +4,78 @@ import { BigPost } from "../components/Posts"; import { Link } from "react-router-dom"; import posts from "../data/posts"; import Comment from "../components/Comment"; +import { getPost, getUser } from "../apis/api"; +import { getCookie } from "../utils/cookie"; +import { deletePost } from "../apis/api"; + +// 수정 const PostDetailPage = () => { - // parameter로 받은 id에 해당하는 post를 찾아서 넣자 - // TODO : api call(get post by id) const { postId } = useParams(); - //라우트에서 변수로 포스트 아이디 받아오는 거 const [post, setPost] = useState(); + const [user, setUser] = useState(); + useEffect(() => { - const post = posts.find((post) => post.id === parseInt(postId)); - //useParams 는 텍스트로 들어가서, 인수로 받으려고 parseInt 한다고 함 - //find 는 배열이 아님, filter 는 배열 - setPost(post); + const getPostAPI = async () => { + const post = await getPost(postId); + setPost(post); + }; + getPostAPI(); }, [postId]); - const onClickDelete = () => { - console.log("delete"); - // add api call for deleting post here - // add redirect to home page + useEffect(() => { + if (getCookie("access_token")) { + const getUserAPI = async () => { + const user = await getUser(); + setUser(user); + }; + getUserAPI(); + } + }, []); + + const onClickDelete = async () => { + const confirmed = window.confirm("삭제할꼬얌?"); + if (confirmed) { + try { + const success = await deletePost(post.id); + if (success) { + console.log("Post deleted successfully"); + } else { + console.log("[ERROR] Error while deleting post"); + } + } catch (error) { + console.error("Error deleting post:", error); + } + window.location.href = "/"; + } }; + console.log(user); + console.log(post); + console.log(user?.id); + console.log(post?.author?.user_id); + console.log(post?.author?.id); + return ( post && (
{/* post detail component */} - +
- - - - + {user?.id === post?.author.id ? ( + <> + + + + + + ) : null}
) @@ -47,12 +83,3 @@ const PostDetailPage = () => { }; export default PostDetailPage; - -{ - /* -onClick={onClickDelete}` -onClick={onClickDelete()}` -onClick={()⇒onClickDelete()}` -onClick 안에는 함수가 들어가야 하기 때문에 2번째것은 return 값이라서 실행이 안됨. -*/ -} diff --git a/src/routes/PostEditPage.jsx b/src/routes/PostEditPage.jsx index 1c49e99..7e560c6 100644 --- a/src/routes/PostEditPage.jsx +++ b/src/routes/PostEditPage.jsx @@ -1,5 +1,81 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { PostForm } from "../components/Form"; +import { Link } from "react-router-dom"; +import { getPost, getTags, updatePost, createTag } from "../apis/api"; + const PostEditPage = () => { - return
여기는 PostEditPage 페이지!
; + const { postId } = useParams(); + const [formData, setFormData] = useState({ + title: "", + content: "", + tags: [], + }); + const navigate = useNavigate(); + + useEffect(() => { + const getPostAPI = async () => { + const post = await getPost(postId); + const postFormData = { + title: post.title, + content: post.content, + tags: post.tags?.map((tag) => tag.content) || [], + }; + setFormData(postFormData); + }; + getPostAPI(); + }, [postId]); + + const [tags, setTags] = useState([]); + useEffect(() => { + const fetchTags = async () => { + try { + const tagsData = await getTags(); + const tagContents = tagsData?.map((tag) => ({ + id: tag.id, + content: tag.content, + })); + setTags(tagContents); + } catch (error) { + console.error("Error fetching tags:", error); + } + }; + fetchTags(); + }, []); + + const handleCreateTag = async (tagContent) => { + try { + const newTag = await createTag({ content: tagContent }); + setTags((prevTags) => [...prevTags, newTag]); + console.log("Tag created successfully"); + } catch (error) { + console.error("Error creating tag:", error); + } + }; + + const onSubmit = async (e) => { + e.preventDefault(); + await updatePost(postId, formData, navigate); + }; + + return ( +
+
+ + + +

Edit Post

+
+ +
+ ); }; export default PostEditPage; diff --git a/src/routes/SignInPage.jsx b/src/routes/SignInPage.jsx index d414839..e688cb1 100644 --- a/src/routes/SignInPage.jsx +++ b/src/routes/SignInPage.jsx @@ -1,5 +1,8 @@ import { useState } from "react"; import { SignInForm } from "../components/Form"; +import { getCookie } from "../utils/cookie"; +import axios from "axios"; +import { signIn } from "../apis/api"; const SignInPage = () => { const [formData, setFormData] = useState({ @@ -12,12 +15,17 @@ const SignInPage = () => { setFormData({ ...formData, [id]: value }); }; - const handleSignInSubmit = () => { - console.log(formData); - alert("로그인 완 료!"); - // add api call for sign in here + const handleSignInSubmit = (e) => { + e.preventDefault(); + signIn(formData); }; + // const handleSignInSubmit = () => { + // console.log(formData); + // alert("로그인 완 료!"); + // // add api call for sign in here + // }; + return (

Sign In

diff --git a/src/routes/SignUpPage.jsx b/src/routes/SignUpPage.jsx index dc0c668..a854412 100644 --- a/src/routes/SignUpPage.jsx +++ b/src/routes/SignUpPage.jsx @@ -1,5 +1,8 @@ import { useState } from "react"; import { SignUpForm } from "../components/Form"; +import axios from "axios"; +import { signUp } from "../apis/api"; + const SignUpPage = () => { const [formData, setFormData] = useState({ email: "", @@ -10,14 +13,18 @@ const SignUpPage = () => { major: "", }); - - const handleSignUpSubmit = () => { - console.log(formData); - alert(`${formData.email}로 회원가입 해 줘`); - // add api call for sign up here - // 보통 api call 하는 친구는 페이지 단에 냅둔다. + const handleSignUpSubmit = (e) => { + e.preventDefault(); + signUp(formData); }; + // const handleSignUpSubmit = () => { + // console.log(formData); + // alert(`${formData.email}로 회원가입 해 줘`); + // // add api call for sign up here + // // 보통 api call 하는 친구는 페이지 단에 냅둔다. + // }; + return (

Sign Up

diff --git a/src/utils/cookie.js b/src/utils/cookie.js new file mode 100644 index 0000000..e5f4ddd --- /dev/null +++ b/src/utils/cookie.js @@ -0,0 +1,21 @@ +// src/utils/cookie.js + +import { Cookies } from "react-cookie"; + +const cookies = new Cookies(); + +// 쿠키 설정하는 함수 +// 궁금하실까봐 만들긴 했는데, 우리는 안 쓸거에요!! (쿠키에 토큰 넣어주는 건 서버에서 해주니까요) +export const setCookie = (name, value, option) => { + return cookies.set(name, value, { ...option }); +}; + +// 쿠키 정보 가져오는 함수 +export const getCookie = (name) => { + return cookies.get(name); +}; + +// 쿠키 정보 삭제하는 함수 +export const removeCookie = (name) => { + cookies.remove(name); +}; From 04d2e01a6774ba7b5a22d79392a506f12cd89c8c Mon Sep 17 00:00:00 2001 From: kimdanran Date: Mon, 29 May 2023 22:34:40 +0900 Subject: [PATCH 3/5] =?UTF-8?q?10=EC=A3=BC=EC=B0=A8=20=EA=B3=BC=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 3 + src/apis/api.js | 44 ++++ src/components/Comment/CommentElement.jsx | 106 ---------- src/components/Form/index.jsx | 90 ++++++++- src/components/Header/index.jsx | 3 + src/routes/MyPage.jsx | 236 ++++++++++++++++++++++ src/routes/PostEditPage.jsx | 2 +- 7 files changed, 371 insertions(+), 113 deletions(-) create mode 100644 src/routes/MyPage.jsx diff --git a/src/App.js b/src/App.js index 3e34fc7..7a5e87f 100644 --- a/src/App.js +++ b/src/App.js @@ -9,6 +9,7 @@ import PostEditPage from "./routes/PostEditPage"; import SignUpPage from "./routes/SignUpPage"; import PostDetailPage from "./routes/PostDetailPage"; import SignInPage from "./routes/SignInPage"; +import MyPage from "./routes/MyPage"; function App() { return ( @@ -28,6 +29,8 @@ function App() { } /> {/* sign up */} } /> + {/* my page */} + } />
diff --git a/src/apis/api.js b/src/apis/api.js index ef62479..32a3c1d 100644 --- a/src/apis/api.js +++ b/src/apis/api.js @@ -155,3 +155,47 @@ export const getUser = async () => { } return response.data; }; + +export const getProfile = async () => { + const response = await instanceWithToken.get("/account/profile/"); + if (response.status === 200) { + console.log("GET USER SUCCESS"); + } else { + console.log("[ERROR] error while updating comment"); + } + return response.data; +}; + +export const updateProfile = async (data) => { + const response = await instanceWithToken.patch("/account/profile/", data); + if (response.status === 200) { + console.log("UPDATE PROFILE SUCCESS"); + } else { + console.log("[ERROR] error while updating comment"); + } + return response.data; +}; + +export const updateUser = async (data) => { + const response = await instanceWithToken.patch("/account/info/", data); + if (response.status === 200) { + console.log("UPDATE USER SUCCESS"); + } else { + console.log("[ERROR] error while updating comment"); + } + return response.data; +}; + +export const deleteTagAPI = async (id) => { + try { + const response = await instanceWithToken.delete(`/tag/${id}/`); + if (response.status === 204) { + console.log("TAG DELETE SUCCESS"); + } else { + console.error(`[ERROR] Unexpected status code: ${response.status}`); + } + } catch (error) { + console.error("[ERROR] error while deleting tag:", error); + throw error; + } +}; diff --git a/src/components/Comment/CommentElement.jsx b/src/components/Comment/CommentElement.jsx index dd2cdc5..bea1c57 100644 --- a/src/components/Comment/CommentElement.jsx +++ b/src/components/Comment/CommentElement.jsx @@ -1,109 +1,3 @@ -// import React from "react"; -// import { useEffect, useState } from "react"; -// import commentList from "../../data/comment"; -// import { getUser } from "../../apis/api"; -// import { getCookie } from "../../utils/cookie"; - -// const CommentElement = ({ -// id, -// post, -// created_at, -// author, -// content, -// deleteComment, -// editComment, -// setEditingCommentId, -// setEditingText, -// editingCommentId, -// editingText, -// handleEditText, -// }) => { -// const [user, setUser] = useState(null); -// const [isEdit, setIsEdit] = useState(false); - -// useEffect(() => { -// // access_token이 있으면 유저 정보 가져옴 -// if (getCookie("access_token")) { -// const getUserAPI = async () => { -// const user = await getUser(); -// setUser(user); -// }; -// getUserAPI(); -// } -// }, []); - -// // comment created_at 전처리 -// const date = new Date(created_at); -// const year = date.getFullYear(); -// let month = date.getMonth() + 1; -// month = month < 10 ? `0${month}` : month; -// let day = date.getDate(); -// day = day < 10 ? `0${day}` : day; - -// const handleEditClick = () => { -// setEditingCommentId(id); -// setEditingText(content); -// }; - -// const handleEditSubmit = () => { -// editComment(id); -// }; - -// return ( -//
-//
-//
-//
-// {editingCommentId === id ? ( -// <> -//
-// -// -//
-// -// ) : ( -// <> -//

{content}

-// -// {year}.{month}.{day} -// -// -// )} -//
-// {user?.id === author ? ( -//
-// {!isEdit && ( -// -// )} -// {editingCommentId !== id && ( -// -// )} -//
-//
{" "} -//
-//
-// ); -// }; - -// export default CommentElement; - import React, { useEffect, useState } from "react"; import { getUser, updateComment } from "../../apis/api"; import { getCookie } from "../../utils/cookie"; diff --git a/src/components/Form/index.jsx b/src/components/Form/index.jsx index e202838..6a51268 100644 --- a/src/components/Form/index.jsx +++ b/src/components/Form/index.jsx @@ -1,4 +1,73 @@ import { useState } from "react"; +import { deleteTagAPI } from "../../apis/api"; + +export const MyPageForm = ({ formData, setFormData, handleMyPageSubmit }) => { + const handleFormData = (e) => { + const { id, value } = e.target; + setFormData({ ...formData, [id]: value }); + }; + + return ( +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+
+ ); +}; export const SignUpForm = ({ formData, setFormData, handleSignUpSubmit }) => { const handleFormData = (e) => { @@ -187,11 +256,20 @@ export const PostForm = ({ setAutoCompletes([]); }; - const deleteTag = (tag) => { - setFormData((prevFormData) => ({ - ...prevFormData, - tags: prevFormData.tags.filter((t) => t !== tag), - })); + const deleteTag = async (tag) => { + try { + // Call the delete API + await deleteTagAPI(tag.id); + + // Update local state + setFormData((prevFormData) => ({ + ...prevFormData, + tags: prevFormData.tags.filter((t) => t.content !== tag.content), + })); + setTags((prevTags) => prevTags.filter((t) => t.content !== tag.content)); + } catch (error) { + console.error("Error deleting tag:", error); + } }; return ( @@ -254,7 +332,7 @@ export const PostForm = ({ {tags && (
{tags.map((tag) => ( -
+

#{tag.content}

diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index 60a95a0..f74aecb 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -40,6 +40,9 @@ const Header = () => { ) : ( <> + + my page + log out diff --git a/src/routes/MyPage.jsx b/src/routes/MyPage.jsx new file mode 100644 index 0000000..20a7dfe --- /dev/null +++ b/src/routes/MyPage.jsx @@ -0,0 +1,236 @@ +import { useEffect, useState } from "react"; +import { getProfile, getUser } from "../apis/api"; +import { getCookie } from "../utils/cookie"; +import { updateProfile } from "../apis/api"; +import { updateUser } from "../apis/api"; +import { SmallPost } from "../components/Posts"; +import { getPosts } from "../apis/api"; +import { getTags } from "../apis/api"; + +// import axios from "axios"; + +const MyPage = () => { + const [user, setUser] = useState(null); + const [postList, setPostList] = useState([]); + const [tags, setTags] = useState([]); + const [searchTags, setSearchTags] = useState([]); + const [searchTagIdList, setSearchTagIdList] = useState([]); + const [formData, setFormData] = useState({ + email: "", + username: "", + college: "", + major: "", + }); + const [isEdit, setIsEdit] = useState(false); + + const toggleSearchTagId = (id) => { + if (searchTagIdList.includes(id)) { + const newSearchTagIdList = searchTagIdList.filter( + (tagId) => tagId !== id + ); + setSearchTagIdList(newSearchTagIdList); + } else { + setSearchTagIdList([...searchTagIdList, id]); + } + }; + + useEffect(() => { + const getProfileAPI = async () => { + const user = await getProfile(); // 백엔드에서 사용자 데이터 가져오는 비동기 함수 + setUser(user); + }; + getProfileAPI(); + }, []); + + useEffect(() => { + if (user) { + setFormData({ + email: user.user.email, + username: user.user.username, + college: user.college, + major: user.major, + }); + } + }, [user]); + + useEffect(() => { + const getPostsAPI = async () => { + const posts = await getPosts(); + setPostList(posts); + }; + getPostsAPI(); + }, []); + + useEffect(() => { + const getPostsAPI = async () => { + const posts = await getPosts(); + setPostList(posts); + }; + getPostsAPI(); + + //추가 + const getTagsAPI = async () => { + const tags = await getTags(); + const tagContents = tags?.map((tag) => { + return { id: tag.id, content: tag.content }; + }); + setTags(tagContents); + setSearchTags(tagContents); + }; + console.log(getTagsAPI); + getTagsAPI(); + // getTags() 이용해서 tag들 불러오고 tags?.map을 이용해서 tagContents에 + // tag.content만 저장한 후, tags와 searchTags에 저장 + }, []); + + console.log(tags); + + const handleChange = (e) => { + const { value } = e.target; + const newTags = tags.filter((tag) => { + return tag.content.includes(value); + }); + setSearchTags(newTags); + console.log(newTags); + }; + + useEffect(() => { + if (searchTagIdList.length === 0) { + setPostList([]); + } else { + const newPostList = postList.filter((post) => + post.tags.some((tag) => searchTagIdList.includes(tag.id)) + ); + setPostList(newPostList); + } + }, [searchTagIdList]); + + const handleInputChange = (e) => { + const { id, value } = e.target; + setFormData((prevFormData) => ({ ...prevFormData, [id]: value })); + }; + + const handleSubmit = async (event, field) => { + event.preventDefault(); // Prevent form submission and page refresh + try { + const updatedData = { + [field]: formData[field], + }; + (await (field === "email" || field === "username")) + ? updateUser(updatedData) + : updateProfile(updatedData); + + setUser((prevUser) => ({ + ...prevUser, + ...updatedData, + })); + window.location.reload(); + } catch (error) { + // Handle the error + } + }; + + return ( + <> +
+

My Page

+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+
+
+
+

My Post

+
+
+ {postList.map( + (post) => + user?.id === post.author.id && ( // Remove curly braces around the condition + + ) + )} +
+ + ); +}; + +export default MyPage; diff --git a/src/routes/PostEditPage.jsx b/src/routes/PostEditPage.jsx index 7e560c6..f2cfea0 100644 --- a/src/routes/PostEditPage.jsx +++ b/src/routes/PostEditPage.jsx @@ -46,7 +46,7 @@ const PostEditPage = () => { const handleCreateTag = async (tagContent) => { try { const newTag = await createTag({ content: tagContent }); - setTags((prevTags) => [...prevTags, newTag]); + setTags((prevTags) => [...prevTags, newTag]); // Append the new tag to the existing tags console.log("Tag created successfully"); } catch (error) { console.error("Error creating tag:", error); From 6a9ff805687521eed2b5fec7e51aba931e621712 Mon Sep 17 00:00:00 2001 From: kimdanran Date: Sat, 17 Jun 2023 14:19:57 +0900 Subject: [PATCH 4/5] 11 weeks assignment --- src/routes/MyPage.jsx | 147 +++++++++++++++++++++++++++++++++++------- 1 file changed, 122 insertions(+), 25 deletions(-) diff --git a/src/routes/MyPage.jsx b/src/routes/MyPage.jsx index 20a7dfe..c784ab8 100644 --- a/src/routes/MyPage.jsx +++ b/src/routes/MyPage.jsx @@ -21,8 +21,32 @@ const MyPage = () => { college: "", major: "", }); + const [editField, setEditField] = useState(""); const [isEdit, setIsEdit] = useState(false); + const handleEditClick = (field) => { + setEditField(field); + setIsEdit(true); + }; + + const handleCancelClick = () => { + setIsEdit(false); + setEditField(""); + setFormData({ + email: user.user.email, + username: user.user.username, + college: user.college, + major: user.major, + }); + }; + + const handleConfirmClick = async (e, field) => { + e.preventDefault(); + setIsEdit(false); + setEditField(""); + handleSubmit(e, field); + }; + const toggleSearchTagId = (id) => { if (searchTagIdList.includes(id)) { const newSearchTagIdList = searchTagIdList.filter( @@ -146,13 +170,31 @@ const MyPage = () => { className="input" value={formData.email} onChange={handleInputChange} + disabled={!isEdit || editField !== "email"} /> - + {isEdit && editField === "email" ? ( + <> + + + + ) : ( + + )}
+ @@ -203,13 +282,31 @@ const MyPage = () => { className="input" value={formData.major} onChange={handleInputChange} + disabled={!isEdit || editField !== "major"} /> - + {isEdit && editField === "major" ? ( + <> + + + + ) : ( + + )}
@@ -219,7 +316,7 @@ const MyPage = () => {
{postList.map( (post) => - user?.id === post.author.id && ( // Remove curly braces around the condition + user.user.username === post.author.username && ( // Remove curly braces around the condition Date: Sat, 17 Jun 2023 17:34:52 +0900 Subject: [PATCH 5/5] build --- .env.development | 1 + .env.production | 1 + src/apis/axios.js | 26 ++++++++++++++++---------- 3 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 .env.development create mode 100644 .env.production diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..df8bde4 --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +REACT_APP_URL= "http://localhost:3000" \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..f4ea7b1 --- /dev/null +++ b/.env.production @@ -0,0 +1 @@ +REACT_APP_URL= "https://port-0-django-koh2xlizm45lt.sel4.cloudtype.app//" \ No newline at end of file diff --git a/src/apis/axios.js b/src/apis/axios.js index bbd4918..68a436b 100644 --- a/src/apis/axios.js +++ b/src/apis/axios.js @@ -3,17 +3,16 @@ import axios from "axios"; import { getCookie } from "../utils/cookie"; -// baseURL, credential, 헤더 세팅 -axios.defaults.baseURL = 'http://localhost:8000/api'; +// baseURL, credential, 헤더 세팅 +axios.defaults.baseURL = "http://localhost:8000/api"; axios.defaults.withCredentials = true; // 쿠키 사용해라 -axios.defaults.headers.post['Content-Type'] = 'application/json'; +axios.defaults.headers.post["Content-Type"] = "application/json"; //post 일때는 이렇게 해라 -axios.defaults.headers.common['X-CSRFToken'] = getCookie('csrftoken'); +axios.defaults.headers.common["X-CSRFToken"] = getCookie("csrftoken"); //공통적으로 쿠키에서 담아와라 - -// 누구나 접근 가능한 API들 +// 누구나 접근 가능한 API들 export const instance = axios.create(); // Token 있어야 접근 가능한 API들 (로그인 등등..) - 얘는 토큰을 넣어줘야 해요 @@ -22,9 +21,9 @@ export const instanceWithToken = axios.create(); // instanceWithToken에는 쿠키에서 토큰을 찾고 담아줍시다! instanceWithToken.interceptors.request.use( // 요청을 보내기전 수행할 일 - // 사실상 이번 세미나에 사용할 부분은 이거밖에 없어요 + // 사실상 이번 세미나에 사용할 부분은 이거밖에 없어요 (config) => { - const accessToken = getCookie('access_token'); + const accessToken = getCookie("access_token"); if (!accessToken) { // token 없으면 리턴 @@ -46,7 +45,7 @@ instanceWithToken.interceptors.request.use( instanceWithToken.interceptors.response.use( (response) => { - // 서버 응답 데이터를 프론트에 넘겨주기 전 수행할 일 + // 서버 응답 데이터를 프론트에 넘겨주기 전 수행할 일 console.log("Interceptor Response!!"); return response; }, @@ -55,4 +54,11 @@ instanceWithToken.interceptors.response.use( console.log("Response Error!!"); return Promise.reject(error); } -); \ No newline at end of file +); + +if (process.env.NODE_ENV === "development") { + axios.defaults.baseURL = "http://localhost:8000/api"; +} else { + axios.defaults.baseURL = + "https://port-0-django-koh2xlizm45lt.sel4.cloudtype.app/api"; +}