diff --git a/package-lock.json b/package-lock.json index 495c411..3ac2ccb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,11 @@ "@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", "web-vitals": "^2.1.4" }, @@ -3116,6 +3119,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.1.tgz", + "integrity": "sha512-YUkWj+xs0oOzBe74OgErsuR3wVn+efrFhXBWrit50kOiED+pvQe2r6MWY0iJMQU/mSVKxvNzL4ZaYvjdX+G7ZA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3811,6 +3822,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", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", @@ -3864,6 +3880,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", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -5087,6 +5112,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", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -8693,6 +8741,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", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -14069,6 +14130,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", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -14213,6 +14279,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", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -14360,6 +14439,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.11.1.tgz", + "integrity": "sha512-OZINSdjJ2WgvAi7hgNLazrEV8SGn6xrKA+MkJe9wVDMZ3zQ6fdJocUjpCUCI0cNrelWjcvon0S/QK/j0NzL3KA==", + "dependencies": { + "@remix-run/router": "1.6.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.11.1.tgz", + "integrity": "sha512-dPC2MhoPeTQ1YUOt5uIK376SMNWbwUxYRWk2ZmTT4fZfwlOvabF8uduRKKJIyfkCZvMgiF0GSCQckmkGGijIrg==", + "dependencies": { + "@remix-run/router": "1.6.1", + "react-router": "6.11.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -16227,6 +16336,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", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", diff --git a/package.json b/package.json index 9ce551a..d3fe664 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,11 @@ "@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", "web-vitals": "^2.1.4" }, diff --git a/src/App.js b/src/App.js index 5f30328..f4334b9 100644 --- a/src/App.js +++ b/src/App.js @@ -1,15 +1,38 @@ import logo from "./logo.svg"; import Header from "./components/Header"; import Footer from "./components/Footer"; -import Home from "./routes/Home"; +import HomePage from "./routes/HomePage"; +import MyPage from "./routes/MyPage"; import "./App.css"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import PostCreatePage from "./routes/PostCreatePage"; +import PostEditPage from "./routes/PostEditPage"; +import SignUpPage from "./routes/SignUpPage"; +import PostDetailPage from "./routes/PostDetailPage"; +import SignInPage from "./routes/SignInPage"; function App() { return (
-
- -
); } diff --git a/src/apis/api.js b/src/apis/api.js new file mode 100644 index 0000000..2c180f6 --- /dev/null +++ b/src/apis/api.js @@ -0,0 +1,185 @@ +import { instance, instanceWithToken } from "./axios"; + +// Account 관련 API들 +export const signIn = async (data) => { + const response = await instance.post("/account/signin/", data); + if (response.status === 200) { + window.location.href = "/"; + } else { + console.log("Error"); + } + }; + +export const signUp = async (data) => { + const response = await instance.post("/account/signup/", data); + if (response.status === 200) { + window.location.href = "/"; + } + return response; + }; + +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; + }; + +export const createPost = async (data, navigate) => { + const response = await instanceWithToken.post("/post/", data); + if (response.status === 201) { + console.log("POST SUCCESS"); + navigate("/"); + } else { + + } + }; + +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 { + + } +}; + + // 과제!! +export const deletePost = async (id, navigate) => { + const response = await instanceWithToken.delete(`/post/${id}/`); + if (response.status === 204) { + console.log("POST DELETE SUCCESS"); + if (window.confirm("정말 글을 삭제하시겠습니까?")) { + navigate("/"); + } + } else { + console.log("[ERROR] error while deleting post"); + console.log(response.status); + } + }; + + // 과제!! +export const likePost = async (postId) => { + const response = await instanceWithToken.post(`/post/${postId}/like/`); + if (response.status === 200) { + console.log("POST LIKE SUCCESS"); + // window.location.reload(); + } else { + console.log("[ERROR] error while liking post"); + // console.log(response.status) + } + + +}; + + +// 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"); + } +}; + +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}/`); + console.log(response) + if (response.status === 204) { + console.log("COMMENT DELETE SUCCESS"); + if (window.confirm("정말 댓글을 삭제하시겠습니까?")) { + 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; + }; + +export const updateInfo = async (data) => { + const response = await instanceWithToken.patch(`/account/profile/`, data); + if (response.status === 200) { + console.log("INFO UPDATE SUCCESS"); + window.location.reload(); + } else { + console.log("[ERROR] error while updating info"); + } +}; + +export const getInfo = async () => { + const response = await instanceWithToken.get("/account/profile/"); + return response.data; +}; + +export const refreshToken = async (token) => { + const response = await instance.post("/account/refresh/", { refresh: token }); + if (response.status === 200) { + console.log("REFRESH TOKEN SUCCESS"); + } else { + console.log("[ERROR] error while refreshing token"); + } + }; + +export const logOut = async (token) => { + const response = await instanceWithToken.post("/account/logout/", { + refresh: token, + }); + if (response.status === 200) { + console.log("REFRESH TOKEN SUCCESS"); + + removeCookie("refresh_token"); + removeCookie("access_token"); + + window.location.reload(); + } else { + console.log("[ERROR] error while refreshing token"); + } + }; + \ No newline at end of file diff --git a/src/apis/axios.js b/src/apis/axios.js new file mode 100644 index 0000000..1b7182c --- /dev/null +++ b/src/apis/axios.js @@ -0,0 +1,60 @@ +import axios from "axios"; +import { getCookie } from "../utils/cookie"; +import { refreshToken } from "./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.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; + }, + async (error) => { + console.log("Response Error!!"); + + const originalRequest = error.config; + if (error.response.status === 401) { //토큰이 만료됨에 따른 에러인지 확인 + const token = getCookie("refresh_token"); + await refreshToken(token); //refresh token 을 활용하여 access token 을 refresh + + return instanceWithToken(originalRequest); //refresh된 access token 을 활용하여 재요청 보내기 + } + return Promise.reject(error); + } + ); \ No newline at end of file diff --git a/src/components/Comment/CommentElement.jsx b/src/components/Comment/CommentElement.jsx new file mode 100644 index 0000000..a2db3b4 --- /dev/null +++ b/src/components/Comment/CommentElement.jsx @@ -0,0 +1,157 @@ +import { useEffect, useState } from "react"; +import { getUser, updateComment, deleteComment } from "../../apis/api"; +import { getCookie } from "../../utils/cookie"; + +const CommentElement = ({ + // comment, + // commentList, + setCommentList, + // commentData, + // setCommentData, + // id, + // // content, + onEdit, + // postId + comment + // handleCommentDelete + + +}) => { + console.log(comment); + // const { comment, handleCommentDelete } = props; + // TODO : props 받기 + // TODO : 수정하는 input 내용 관리 + const [content, setContent] = useState(comment.content); + const [isEdit, setIsEdit] = useState(false); + const [user, setUser] = useState(null); + + // comment created_at 전처리 + console.log("왔다"); + + console.log(comment); + const date = new Date(comment.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 [isOnClickEdit, setIsOnClickEdit] = useState(false); + const [editedComment, setEditedComment] = useState(content); + + const handleEditComment = () => { + updateComment(comment.id, { content: content }); + + }; + + useEffect(() => { + if (getCookie("access_token")) { + const getUserAPI = async () => { + const user = await getUser(); + setUser(user); + }; + getUserAPI(); + } + }, []); + + const handleCommentDelete = async () => { + // console.log(commentList); + // console.log(id); + // const newCL = commentList.filter((c) => c.id !== id); + // console.log(newCL); + // setCommentList(newCL); + // console.log(comment.id); + // deleteComment(comment.id); + + deleteComment(comment.id); + + }; + + // const onClickEdit = () => { + // setIsOnClickEdit(true); + // }; + + // const clickEdit = (e) => { + // const editCL = e.target.value; + // setEditedComment(editCL); + // }; + + // const onDoneClick = () => { + // onEdit(id, editedComment); + // setIsOnClickEdit(false); + // }; + + // const handleCommentDelete = async () => { + // // console.log(commentList); + // // console.log(id); + // // const newCL = commentList.filter((c) => c.id !== id); + // // console.log(newCL); + // // setCommentList(newCL); + // // console.log(comment.id); + // // deleteComment(comment.id); + // deleteComment(comment.id); + + // }; + // console.log(comment.id); + + // useEffect(() => { + // const deleteCommentAPI = async () => { + // const deletecomment = await deleteComment(pos); + + // } + // deleteCommentAPI(); + + // },[onClickDelete]); + + // console.log(postId); + + return ( +
+
+ {isEdit ? ( + setContent(e.target.value)} + /> + ) : ( +

{comment.content}

+ )} + {/* // 날짜 */} + + {year}.{month}.{day} + +
+ {/* // 수정, 삭제버튼 */} + {user?.id === comment.author.id ? ( +
+ {isEdit ? ( + <> + + + + + ) : ( + <> + + + + )} +
+ ) : null} +
+ ); +}; + +export default CommentElement; diff --git a/src/components/Comment/index.jsx b/src/components/Comment/index.jsx new file mode 100644 index 0000000..555efcd --- /dev/null +++ b/src/components/Comment/index.jsx @@ -0,0 +1,119 @@ +import { useState, useEffect } from "react"; +import CommentElement from "./CommentElement"; +import { getComments } from "../../apis/api"; +import { createComment, deleteComment } from "../../apis/api"; +import { getCookie } from "../../utils/cookie"; + +const Comment = ({postId}) => { + // TODO 1: comments 불러와서 저장해야겟즤 + const [commentList, setCommentList] = useState([]); + // TODO 2: comment추가하는 input 관리해줘야겟지 + // TODO 3: comment Form 제출됐을때 실행되는 함수 만들어줘 + // const [commentData, setCommentData] = useState(""); + const [newContent, setNewContent] = useState(""); + + const editComment = (e) => { + setNewContent(e.target.value); + console.log("여기"); + // setCommentData(""); + }; + console.log(newContent); + + useEffect(() => { + const getCommentsAPI = async () => { + const comments = await getComments(postId); + setCommentList(comments); + } + getCommentsAPI(); + + },[postId]); + console.log(postId); + + // useEffect(() => { + // const deleteCommentAPI = async () => { + // const deletecomment = await deleteComment(postId); + // setCommentList(deletecomment); + // } + // deleteCommentAPI(); + + // },[onClickDelete]); + // console.log(postId); + + const handleCommentSubmit = (e) => { + e.preventDefault(); + + createComment({ post: postId, content: newContent }); + setNewContent(""); + }; + + const handleEditComment = (id, newContent) => { + setCommentList((prevCommentList) => { + const updatedList = prevCommentList.map((comment) => { + if (comment.id === id) { + return { ...comment, content: newContent }; + } + return comment; + }); + return updatedList; + }); + }; + + const handleCommentDelete = async (e) => { + // console.log(commentList); + // console.log(id); + // const newCL = commentList.filter((c) => c.id !== id); + // console.log(newCL); + // setCommentList(newCL); + // console.log(comment.id); + // deleteComment(comment.id); + + deleteComment(comment.id); + + }; + + return ( +
+

Comments

+ {commentList && commentList.map((c) => ( + + ))} + {/*
*/} + + {/* // TODO 2-3 : comment 추가하는 comment form 만들어주기 */} + + +
+
+ ); +}; + +export { Comment }; diff --git a/src/components/Form/index.jsx b/src/components/Form/index.jsx new file mode 100644 index 0000000..9b97efc --- /dev/null +++ b/src/components/Form/index.jsx @@ -0,0 +1,261 @@ +import { useState } from "react"; + +export const SignUpForm = ({ formData, setFormData, handleSignUpSubmit }) => { + const handleFormData = (e) => { + const { id, value } = e.target; + setFormData({ ...formData, [id]: value }); + }; + + return ( +
+ + + + + + + + + + + + + + + + + + +
+ ); +}; + +export const SignInForm = ({ formData, setFormData, handleSignInSubmit }) => { + const handleFormData = (e) => { + const { id, value } = e.target; + setFormData({ ...formData, [id]: value }); + }; + + return ( +
+ + + + + +
+ ); +}; + +export const PostForm = ({ onSubmit, tags, formData, setFormData }) => { + //태그 Input 안에 값 + const [tagInputValue, setTagInputValue] = useState(""); + + //자동완성 태그들 + const [autoCompletes, setAutoCompletes] = useState([]); + + const handleChange = (e) => { + setFormData({ ...formData, [e.target.id]: e.target.value }); + }; + + //태그 인풋 값 바뀌면 그에 따라서 자동 완성값들도 변경 + const handleTag = (e) => { + setTagInputValue(e.target.value); + if (e.target.value) { + const autoCompleteData = tags.filter((tag) => + tag.includes(e.target.value) + ); + setAutoCompletes(autoCompleteData); + } + }; + + // 자동완성 값이 있는 버튼을 눌렀을 때 이를 태그에 등록 + const handleAutoCompletes = (autoComplete) => { + const selectedTag = tags.find((tag) => tag === autoComplete); + + if (formData.tags.includes(selectedTag)) return; + + setFormData({ + ...formData, + tags: [...formData.tags, selectedTag], + }); + setTagInputValue(""); + setAutoCompletes([]); + }; + + // 추가 버튼 혹인 엔터 누르면 태그 생성 + const addTag = (e) => { + e.preventDefault(); + + // 입력한 내용이 이미 등록된 태그면 그냥 등록 안됨 + if (formData.tags.find((tag) => tag === tagInputValue)) return; + + setFormData({ + ...formData, + tags: [...formData.tags, tagInputValue], + }); + + setTagInputValue(""); + setAutoCompletes([]); + }; + + // X버튼 눌렀을때 태그 삭제 + const deleteTag = (tag) => { + setFormData({ + ...formData, + tags: formData.tags.filter((t) => t !== tag), + }); + }; + return ( +
+ + + + + +
+
+ + +
+
+ {autoCompletes && + autoCompletes.map((autoComplete) => ( + + ))} +
+
+ {formData.tags && ( +
+ {formData.tags.map((tag) => ( +
+ +

#{tag}

+
+ {/* 삭제버튼 */} +
+ ))} +
+ )} + +
+ ); +}; diff --git a/src/components/Header/Header.css b/src/components/Header/Header.css deleted file mode 100644 index f80b333..0000000 --- a/src/components/Header/Header.css +++ /dev/null @@ -1,13 +0,0 @@ -#header-wrapper { - width: 100%; - height: 80px; - padding: 10px 80px; - display: flex; - align-items: center; - gap: 20px; - background-color: rgba(0, 0, 0, 0.1); -} - -#header-lion { - max-width: 60px; -} diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index 2110ea0..75b5999 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -1,10 +1,50 @@ +import { Link } from "react-router-dom"; import lion from "../../assets/images/lion.jpeg"; -import "./Header.css"; +import { useState, useEffect } from "react"; +import { getCookie, removeCookie } from "../../utils/cookie"; +// import "./Header.css"; 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 = "/"; // 새로고침 - 로그아웃 되었다는 것을 인지시켜주기 위해 + const token = getCookie("refresh_token"); + logOut(token); + }; return ( -
+
- lion + lion + Snulion Blog +
+
+ {!isLoggedIn ? ( + <> + + sign In + + + sign up + + + ) : ( + <> + + my page + + + log out + + + )} +
); diff --git a/src/components/Posts/index.jsx b/src/components/Posts/index.jsx index f024c63..2b30085 100644 --- a/src/components/Posts/index.jsx +++ b/src/components/Posts/index.jsx @@ -1,7 +1,20 @@ -export const SmallPost = ({ post }) => { - const onClickLike = () => { +import { Link } from "react-router-dom" +import { likePost } from "../../apis/api"; +import { useState } from "react"; + + +export const SmallPost = ({ post, setPostList }) => { + const [likeCount, setLikeCount] = useState(post.like_users.length); + const onClickLike = async () => { console.log("나도 좋아!"); // add api call for liking post here + const liking = await likePost(post.id); + setPostList((prevPostList) => + prevPostList.map((p) => (p.id === post.id ? response : p)) + ); + // setLikeCount(liking); + + }; return ( @@ -16,8 +29,52 @@ export const SmallPost = ({ post }) => { ))}
- {post.like_users.length > 0 && `❤️ ${post.like_users.length}`} + ❤️ {post.like_users.length > 0 && `${post.like_users.length}`} + {/* {`❤️ ${post.like_users.length}`} */} + {/* ❤️ {likeCount} */} +
+ +
+ detail +
+ + + ); +}; + + + +export const BigPost = ({ post }) => { + const [likeCount, setLikeCount] = useState(post.like_users.length); + const onClickLike = () => { + console.log("나도 좋아!"); + // add api call for liking post here + const liking = likePost(post.id); + setLikeCount(post.like_users.length); + + }; + + return ( +
+
+

{post.title}

+ {post.author.username} +
{post.content}
+
+ {post.tags && + post.tags.map((tag) => ( + + #{tag.content} + + ))} +
+
+ ❤️ {post.like_users.length > 0 && `${post.like_users.length}`} + {/* ❤️ {post.like_users.length} */} + {/* ❤️ {likeCount} */} +
); }; + diff --git a/src/data/comments.js b/src/data/comments.js new file mode 100644 index 0000000..677f950 --- /dev/null +++ b/src/data/comments.js @@ -0,0 +1,25 @@ +//가짜 comment data 넣어주기 +const comments = [ + { + "id": 1, + "content": "멋사 11기 화이팅!!", + "created_at": "2023-02-12T15:09:43Z", + "post": 1, + "author": { + "id": 2, + "username": "user2" + } + }, + { + "id": 2, + "content": "멋사 좋아요~~", + "created_at": "2022-11-22T15:09:43Z", + "post": 1, + "author": { + "id": 2, + "username": "user2" + } + } +] + +export default comments; \ No newline at end of file diff --git a/src/index.css b/src/index.css index 5b2de4d..644d635 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-orange-400 text-white font-medium hover:text-black rounded-xl text-lg p-2.5; } /* 모든 form 일괄 적용 */ .form { @@ -25,6 +25,10 @@ @apply border-2 border-white w-full px-6 py-3 rounded-2xl text-white bg-transparent placeholder-opacity-50 focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-transparent; } +.textDesign{ + @apply border-b border-gray-300 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-200 px-4 py-2 w-full; +} + body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", diff --git a/src/routes/Home.jsx b/src/routes/Home.jsx deleted file mode 100644 index ce552fd..0000000 --- a/src/routes/Home.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useEffect, useState } from "react"; -import { SmallPost } from "../components/Posts"; -import posts from "../data/posts"; - -const Home = () => { - const [tags, setTags] = useState([]); - const [searchTags, setSearchTags] = useState([]); - const [searchValue, setSearchValue] = useState(""); - const [postList, setPostList] = useState(posts); - - useEffect(() => { - const tagList = posts.reduce((acc, post) => { - for (let tag of post.tags) { - acc.add(tag.content); - } - return acc; - }, new Set()); - setTags([...tagList]); - setSearchTags([...tagList]); - }, []); - - const handleChange = (e) => { - const { value } = e.target; - const newTags = tags.filter((tag) => tag.includes(value)); - setSearchTags(newTags); - }; - - const handleTagFilter = (e) => { - let tag = e.target.innerText.slice(1); - if (searchValue === tag) { - const clickedtag = tags.filter((t) => { - t !== tag; - }); - setSearchValue(clickedtag); - setPostList(posts); - } else { - // let tmp = searchValue.push(tag); - setSearchValue(tag); - let clickedpost = posts.filter((post) => { - let include = false; - post.tags.map((t) => { - if (t.content === tag) include = true; - // console.log(t.content); - // console.log(tag); - }); - return include; - // console.log(post.tags); - }); - // console.log(clickedpost); - setPostList(clickedpost); - } - }; - - return ( -
-
-
-

my blog

-
- -
- {searchTags.map((tag) => { - return ( - - ); - })} -
-
- -
- {postList.map((post) => ( - - ))} -
-
- ); -}; - -export default Home; diff --git a/src/routes/HomePage.jsx b/src/routes/HomePage.jsx new file mode 100644 index 0000000..1103d8d --- /dev/null +++ b/src/routes/HomePage.jsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from "react"; +import { SmallPost } from "../components/Posts"; +import { Link } from "react-router-dom"; +import { getTags, getPosts } from "../apis/api"; +import { getCookie } from "../utils/cookie"; +// import axios from "axios"; + +const HomePage = () => { + const [tags, setTags] = useState([]); + const [searchTags, setSearchTags] = useState([]); + const [searchValue, setSearchValue] = useState(""); + const [postList, setPostList] = useState([]); + + // useEffect( () => { + // const getPostAPI = async () => { + // const response = await axios.get("http://localhost:8000/api/post/"); + // console.log(response); + // } + // getPostAPI(); + // }, []); + useEffect(() => { + const getPostAPI = async () => { + const posts = await getPosts(); + setPostList(posts); + } + getPostAPI(); + + + + //추가 + const getTagsAPI = async () => { + const tags = await getTags(); + const tagContents = tags.map((tag) => { + return tag.content; + }); + setTags(tagContents); + setSearchTags(tagContents); + }; + getTagsAPI(); + // getTags() 이용해서 tag들 불러오고 tags.map을 이용해서 tagContents에 + // tag.content만 저장한 후, tags와 searchTags에 저장 + }, []); + + const handleChange = (e) => { + const { value } = e.target; + const newTags = tags.filter((tag) => tag.includes(value)); + setSearchTags(newTags); + }; + + const handleTagFilter = (e) => { + // let tag = e.target.innerText.slice(1); + // if (searchValue === tag) { + // const clickedtag = tags.filter((t) => { + // t !== tag; + // }); + // setSearchValue(clickedtag); + // // setPostList(posts); + // } else { + // // let tmp = searchValue.push(tag); + // setSearchValue(tag); + // let clickedpost = posts.filter((post) => { + // let include = false; + // post.tags.map((t) => { + // if (t.content === tag) include = true; + // // console.log(t.content); + // // console.log(tag); + // }); + // return include; + // // console.log(post.tags); + // }); + // // console.log(clickedpost); + // setPostList(clickedpost); + // } + const { innerText } = e.target; + if (searchValue === innerText.substring(1)) { + setSearchValue(""); + } else { + const activeTag = innerText.substring(1); + setSearchValue(activeTag); + } + }; + + return ( +
+
+
+

my blog

+
+ +
+ {searchTags.map((tag) => { + return ( + + ); + })} +
+
+ +
+ {postList + .filter((post) => + searchValue + ? post.tags.find((tag) => tag.content === searchValue) + : post + ) + .map((post) => ( + + ))} +
+ + {getCookie("access_token") ? ( +
+ + Post + +
+ ) : null} + + {/*
+ + Post + +
*/} +
+ ); +}; + +export default HomePage; diff --git a/src/routes/MyPage.jsx b/src/routes/MyPage.jsx new file mode 100644 index 0000000..507c54b --- /dev/null +++ b/src/routes/MyPage.jsx @@ -0,0 +1,256 @@ +import { useState, useEffect } from "react"; +import { getInfo, updateInfo, getPosts, getTags } from "../apis/api"; +import { SmallPost } from "../components/Posts"; + +const MyPage = () => { + const [formData, setFormData] = useState({ + email: "", + username: "", + college: "", + major: "", + }); + + const [originalData, setOriginalData] = useState({}); + + const [isEdit, setIsEdit] = useState({ + email: false, + username: false, + college: false, + major: false, + }); + + const [postList, setPostList] = useState([]); + const [id, setId] = useState(""); + const [tags, setTags] = useState([]); + + useEffect(() => { + const getPostAPI = async () => { + const posts = await getPosts(); + setPostList(posts); + }; + getPostAPI(); + + //추가 + const getTagsAPI = async () => { + const tags = await getTags(); + const tagContents = tags.map((tag) => { + return tag.content; + }); + setTags(tagContents); + }; + getTagsAPI(); + // getTags() 이용해서 tag들 불러오고 tags.map을 이용해서 tagContents에 + // tag.content만 저장한 후, tags와 searchTags에 저장 + }, []); + + useEffect(() => { + const getProfileAPI = async () => { + const profile = await getInfo(); + console.log(profile); + setFormData({ + email: profile.user.email, + username: profile.user.username, + college: profile.college, + major: profile.major, + }); + setOriginalData({ + email: profile.user.email, + username: profile.user.username, + college: profile.college, + major: profile.major, + }); + setId(profile.user.id); + }; + getProfileAPI(); + }, []); + + const handleEditInfo = () => { + updateInfo(formData); + }; + + const handleCancelEdit = (field) => { + setFormData({ ...formData, [field]: originalData[field] }); + setIsEdit({ ...isEdit, [field]: false }); + }; + + console.log(postList); + console.log(id); + return ( +
+

My Info

+ +
email:
+ {isEdit.email ? ( + <> + + setFormData({ ...formData, email: e.target.value }) + } + /> + + + + + + + ) : ( + <> +
+ +

{formData.email}

+ + + {/*
*/} +
+ + )} + + {/* */} +
username:
+ + {isEdit.username ? ( + <> + + setFormData({ ...formData, username: e.target.value }) + } + /> + + + + + + + ) : ( + <> +
+

{formData.username}

+ +
+ + )} + +
college:
+ {isEdit.college ? ( + <> + + setFormData({ ...formData, college: e.target.value }) + } + /> + + + + + + + ) : ( + <> +
+

{formData.college}

+ +
+ + )} + +
major:
+ {isEdit.major ? ( + <> + + setFormData({ ...formData, major: e.target.value }) + } + /> + + + + + + + ) : ( + <> +
+

{formData.major}

+ +
+ + )} + {/* */} + +

My Posts

+
+ {postList + .filter((post) => post.author.id === id) + .map((post) => ( + + ))} +
+ + ); +}; + +export default MyPage; diff --git a/src/routes/PostCreatePage.jsx b/src/routes/PostCreatePage.jsx new file mode 100644 index 0000000..fbfc812 --- /dev/null +++ b/src/routes/PostCreatePage.jsx @@ -0,0 +1,71 @@ +import { useEffect, useState } from "react"; +import { BigPost } from "../components/Posts"; +import posts from "../data/posts"; +import { PostForm } from "../components/Form"; +import { getTags, createPost } from "../apis/api"; +import { useNavigate } from "react-router-dom"; + + +const PostCreatePage = () => { + const [isSubmitted, setIsSubmitted] = useState(false); + // 화면그리기 + const [formData, setFormData] = useState({ + title: "", + content: "", + tags: [], + }); + + const [tags, setTags] = useState([]); + useEffect(() => { + const getTagsAPI = async () => { + const tags = await getTags(); + const tagContents = tags.map((tag) => { + return tag.content; + }); + setTags(tagContents); + }; + getTagsAPI(); + }, []); + + // const navigate = useNavigate(); + +// const onSubmit = (e) => { +// e.preventDefault(); +// createPost(formData, navigate); +// }; + +// 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 navigate = useNavigate(); + +const onSubmit = (e) => { + e.preventDefault(); + createPost(formData, navigate); + }; + + return ( +
+

New Post

+ +
+ ); + }; + +export default PostCreatePage; \ No newline at end of file diff --git a/src/routes/PostDetailPage.jsx b/src/routes/PostDetailPage.jsx new file mode 100644 index 0000000..4b4ecfc --- /dev/null +++ b/src/routes/PostDetailPage.jsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from "react"; +import { useNavigation, useParams } from "react-router-dom"; +import { BigPost } from "../components/Posts"; +import { Comment } from "../components/Comment/index"; +import { Link } from "react-router-dom"; +import posts from "../data/posts"; +import { deletePost, getComments } from "../apis/api"; +import { getPost, getUser } from "../apis/api"; +import { getCookie } from "../utils/cookie"; +import { useNavigate } from "react-router-dom"; + +const PostDetailPage = () => { + const { postId } = useParams(); + const [post, setPost] = useState(); + const [comment, setComment] = useState(); + const [user, setUser] = useState(); + + useEffect(() => { + const post = posts.find((post) => post.id === parseInt(postId)); + setPost(post); + }, [postId]); + + useEffect(() => { + const getPostAPI = async () => { + const post = await getPost(postId); + setPost(post); + } + getPostAPI(); + },[postId]); + console.log(postId); + + useEffect(() => { + // access_token이 있으면 유저 정보 가져옴 + if (getCookie("access_token")) { + const getUserAPI = async () => { + const user = await getUser(); + setUser(user); + }; + getUserAPI(); + } + }, []); + + const navigate = useNavigate(); + const onClickDelete = () => { + deletePost(postId, navigate); + + }; + console.log("delete"); + + return ( + post && ( +
+ {/* post detail component */} + + +
+ {user?.id === post?.author.id ? ( + <> + + + + + + ) : null} + {/* user와 post.author가 동일하면 버튼을 리턴, 아니면 null */} +
+ {/* 수정 */} +
+ ) + ); +}; + +export default PostDetailPage; diff --git a/src/routes/PostEditPage.jsx b/src/routes/PostEditPage.jsx new file mode 100644 index 0000000..f2d6eb3 --- /dev/null +++ b/src/routes/PostEditPage.jsx @@ -0,0 +1,61 @@ +import { getPost } from "../apis/api"; + +const PostEditPage = () => { + const { postId } = useParams(); + + const [formData, setFormData] = useState({ + title: "", + content: "", + tags: [], + }); + + useEffect(() => { + const getPostAPI = async () => { + const post = await getPost(postId); + const postFormData = { + ...post, + tags: post.tags.map((tag) => tag.content), + }; + setFormData(postFormData); + }; + getPostAPI(); + }, [postId]); + + const [tags, setTags] = useState([]); + useEffect(() => { + const getTagsAPI = async () => { + const tags = await getTags(); + const tagContents = tags.map((tag) => { + return tag.content; + }); + setTags(tagContents); + }; + getTagsAPI(); + }, []); + + const navigate = useNavigate(); + +const onSubmit = (e) => { + e.preventDefault(); + updatePost(postId, formData, navigate); + }; + + return ( +
+
+ + + +

Edit Post

+
+ +
+ ); +}; + +export default PostEditPage; \ No newline at end of file diff --git a/src/routes/SignInPage.jsx b/src/routes/SignInPage.jsx new file mode 100644 index 0000000..c590108 --- /dev/null +++ b/src/routes/SignInPage.jsx @@ -0,0 +1,32 @@ +import { useState } from "react"; +import { SignInForm } from "../components/Form"; +import { signIn } from "../apis/api"; +const SignInPage = () => { + const [formData, setFormData] = useState({ + username: "", + password: "", + }); + + const handleSignInSubmit = (e) => { + e.preventDefault(); + signIn(formData); + // console.log(formData); + // alert("로그인 완 료!"); + // add api call for sign in here + // const signinMem = await signIn(formData); + // console.log(signinMem); + + }; + return ( +
+

Sign In

+ +
+ ); +}; + +export default SignInPage; \ No newline at end of file diff --git a/src/routes/SignUpPage.jsx b/src/routes/SignUpPage.jsx new file mode 100644 index 0000000..43789b7 --- /dev/null +++ b/src/routes/SignUpPage.jsx @@ -0,0 +1,47 @@ +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: "", + password: "", + confirm_password: "", + username: "", + college: "", + major: "", + }); + + const handleSignUpSubmit = (e) => { + e.preventDefault(); + signUp(formData); + // const response = await axios.post("http://localhost:8000/api/account/signup/", formData, { + // headers: { + // "Content-Type": "application/json", + // "X-CSRFToken": getCookie("csrftoken"), + // }, + // withCredentials: true + // }); + // console.log(response); + // console.log(formData); + // alert(`${formData.email}로 회원가입 해 줘`); + // // add api call for sign up here + // const signupMem = await signUp(formData); + // console.log(signupMem); + + }; + return ( +
+

Sign Up

+ +
+ ); +}; + +export default SignUpPage; \ No newline at end of file diff --git a/src/utils/cookie.js b/src/utils/cookie.js new file mode 100644 index 0000000..aeae0dd --- /dev/null +++ b/src/utils/cookie.js @@ -0,0 +1,19 @@ +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) +} \ No newline at end of file