-
-
{article.likeCount}
+
+
+
+
+
+
+ {article.title}
+
+
+
-
- {createDateStringWithDot(article.createdAt)}
+
+
+
{article.writer.nickname}
+
+
+
{article.likeCount}
+
+
+
+ {createDateStringWithDot(article.createdAt)}
+
-
-
+
+
);
}
diff --git a/src/components/boards/BestPosts.tsx b/src/components/boards/BestPosts.tsx
index 5f513d5e9..e245bd811 100644
--- a/src/components/boards/BestPosts.tsx
+++ b/src/components/boards/BestPosts.tsx
@@ -1,4 +1,4 @@
-import getArticles, { Article } from "@/apis/getArticles";
+import { Article, getArticles } from "@/axios/articles";
import usePageSize from "@/hooks/usePageSize";
import { useState, useEffect, useCallback } from "react";
import BestPostCard from "./BestPostCard";
diff --git a/src/components/boards/PostList.tsx b/src/components/boards/PostList.tsx
index ed6eb4f65..6945d09f4 100644
--- a/src/components/boards/PostList.tsx
+++ b/src/components/boards/PostList.tsx
@@ -1,11 +1,11 @@
import PostListHeader from "./PostListHeader";
-import getArticles, { Article, orderOption } from "@/apis/getArticles";
+import { Article, OrderOption, getArticles } from "@/axios/articles";
import { useState, useEffect, useCallback } from "react";
import BasicPostCard from "./BasicPostCard";
export default function PostList() {
const [articles, setArticles] = useState
([]);
- const [orderOption, setOrderOption] = useState("recent");
+ const [orderOption, setOrderOption] = useState("recent");
const [currentKeyword, setCurrentKeyword] = useState("");
const handleAllArticleLoad = useCallback(async () => {
diff --git a/src/components/boards/PostListHeader.tsx b/src/components/boards/PostListHeader.tsx
index bf2b241da..f8e81de55 100644
--- a/src/components/boards/PostListHeader.tsx
+++ b/src/components/boards/PostListHeader.tsx
@@ -1,12 +1,14 @@
import React from "react";
-import { orderOption } from "@/apis/getArticles";
+import { OrderOption } from "@/axios/articles";
import Dropdown from "../@shared/Dropdown";
import SearchBar, { SearchBarProps } from "../@shared/SearchBar";
import BlueButton from "../@shared/BlueButton";
+import Link from "next/link";
+import { useRouter } from "next/router";
interface PostListHeader extends SearchBarProps {
- currentOrderOption: orderOption;
- onOrderChange: React.Dispatch>;
+ currentOrderOption: OrderOption;
+ onOrderChange: React.Dispatch>;
}
const ORDER_ITEM_DICT = {
@@ -19,12 +21,15 @@ export default function PostListHeader({
onOrderChange,
onKeywordChange,
}: PostListHeader) {
+ const { pathname } = useRouter();
return (
diff --git a/src/components/boards/id/PostCommentCard.tsx b/src/components/boards/id/PostCommentCard.tsx
new file mode 100644
index 000000000..e94593dec
--- /dev/null
+++ b/src/components/boards/id/PostCommentCard.tsx
@@ -0,0 +1,35 @@
+import { Comment } from "@/axios/comments";
+import elapsedTimeCalc from "@/utils/elapsedTimeCalc";
+import Image from "next/image";
+
+interface PostCommentCardProps {
+ comment: Comment;
+}
+
+export default function PostCommentCard({ comment }: PostCommentCardProps) {
+ const { content, writer, createdAt } = comment;
+
+ return (
+
+
+
+
+
+
+
+
{writer.nickname}
+
{elapsedTimeCalc(createdAt)}
+
+
+
+ );
+}
diff --git a/src/components/boards/id/PostCommentForm.tsx b/src/components/boards/id/PostCommentForm.tsx
new file mode 100644
index 000000000..ec55b8730
--- /dev/null
+++ b/src/components/boards/id/PostCommentForm.tsx
@@ -0,0 +1,49 @@
+import BlueButton from "@/components/@shared/BlueButton";
+import GrayTextarea from "@/components/@shared/inputs/GrayTextarea";
+import { useRouter } from "next/router";
+import React from "react";
+import { Comment, postArticleComment } from "@/axios/comments";
+
+interface PostCommentFormProps {
+ onChangeComments: React.Dispatch
>;
+}
+
+export default function PostCommentForm({ onChangeComments }: PostCommentFormProps) {
+ const { query } = useRouter();
+ const [currentContent, setCurrentContent] = React.useState("");
+
+ const handleTextAreaChange = (e: React.ChangeEvent) => {
+ const contentValue = e.target.value;
+
+ setCurrentContent(contentValue);
+ };
+
+ const handleFormSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ try {
+ const postedComment = await postArticleComment({
+ articleId: Number(query.id),
+ content: currentContent,
+ });
+
+ onChangeComments((prevComments) => [postedComment, ...prevComments]);
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/boards/id/PostDetail.tsx b/src/components/boards/id/PostDetail.tsx
new file mode 100644
index 000000000..fa4eedd00
--- /dev/null
+++ b/src/components/boards/id/PostDetail.tsx
@@ -0,0 +1,39 @@
+import { Article } from "@/axios/articles";
+import createDateStringWithDot from "@/utils/createDateStringWithDot";
+import Image from "next/image";
+
+interface PostDetailProps {
+ article: Article;
+}
+
+export default function PostDetail({ article }: PostDetailProps) {
+ const { title, content, likeCount, writer, createdAt } = article;
+ return (
+
+
+
{title}
+
+
+
+
+
+
{writer.nickname}
+
+ {createDateStringWithDot(createdAt)}
+
+
+
+
+
+ {content}
+
+
+ );
+}
diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx
index 057c8a57b..5a7e49c44 100644
--- a/src/pages/_document.tsx
+++ b/src/pages/_document.tsx
@@ -3,7 +3,14 @@ import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
-
+
+
+
diff --git a/src/pages/boards/[id].tsx b/src/pages/boards/[id].tsx
index 4eed52e16..1c5c501ad 100644
--- a/src/pages/boards/[id].tsx
+++ b/src/pages/boards/[id].tsx
@@ -1,3 +1,78 @@
-export default function BoardDetail() {
- return 게시글세부사항
;
+import { getArticleByID, Article } from "@/axios/articles";
+import { getArticleComments, ArticleCommentsResponse } from "@/axios/comments";
+import React from "react";
+import { GetServerSidePropsContext } from "next";
+import GlobalNavBar from "@/components/@shared/GlobalNavBar";
+import PostDetail from "@/components/boards/id/PostDetail";
+import PostCommentForm from "@/components/boards/id/PostCommentForm";
+import PostCommentCard from "@/components/boards/id/PostCommentCard";
+import BlueButton from "@/components/@shared/BlueButton";
+import Image from "next/image";
+import Link from "next/link";
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const id = context.params ? Number(context.params["id"]) : null;
+ try {
+ if (id === null) {
+ throw new Error("id 값이 올바르지 않습니다.");
+ }
+ const article = await getArticleByID({ articleId: id });
+ const commentsRes = await getArticleComments({ articleId: id, limit: 10 });
+ return { props: { article, commentsRes } };
+ } catch {
+ return { notFound: true };
+ }
+}
+
+function NoComments() {
+ return (
+
+
+
+ );
+}
+
+function BackToBoardsPageButton() {
+ return (
+
+
+ 목록으로 돌아가기
+
+
+
+ );
+}
+
+interface PostDetailPageProps {
+ article: Article;
+ commentsRes: ArticleCommentsResponse;
+}
+
+export default function PostDetailPage({ article, commentsRes }: PostDetailPageProps) {
+ const { nextCursor, list } = commentsRes;
+ const [comments, setComments] = React.useState(list);
+ const [cursor, setCursor] = React.useState(nextCursor);
+
+ return (
+ <>
+
+
+
+
+ {comments.length !== 0 ? (
+
+ {comments.map((comment) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+
+ >
+ );
}
diff --git a/src/pages/boards/add.tsx b/src/pages/boards/add.tsx
index d6a7bd1f1..992a0d2ea 100644
--- a/src/pages/boards/add.tsx
+++ b/src/pages/boards/add.tsx
@@ -1,3 +1,152 @@
+import { postArticle, PostArticleProps } from "@/axios/articles";
+import BlueButton from "@/components/@shared/BlueButton";
+import GlobalNavBar from "@/components/@shared/GlobalNavBar";
+import GrayInput from "@/components/@shared/inputs/GrayInput";
+import React from "react";
+import FileInput from "@/components/@shared/inputs/FileInput";
+import GrayTextarea from "@/components/@shared/inputs/GrayTextarea";
+import uploadImage from "@/axios/images";
+
+interface TextInputProps {
+ onInputChange: (e: React.ChangeEvent) => void;
+}
+
+function TextInput({ onInputChange }: TextInputProps) {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+}
+
export default function AddBoard() {
- return 게시글추가하기
;
+ const [articleValues, setArticleValues] = React.useState({
+ image: null,
+ content: "",
+ title: "",
+ });
+ const [currentImgFile, setCurrentImgFile] = React.useState(null);
+ const [previewImg, setPreviewImg] = React.useState(null);
+
+ /**
+ * text input change 이벤트 핸들러
+ * text input 값을 articleValues에 최신화 함
+ */
+ const handleTextInputChange = (e: React.ChangeEvent) => {
+ const currentTargetId = e.target.id;
+ const currentValue = e.target.value;
+
+ setArticleValues((prevArticleValues) => ({
+ ...prevArticleValues,
+ [currentTargetId]: currentValue,
+ }));
+ };
+
+ /**
+ * image file input change 이벤트 핸들러
+ * image file input 값을 currentImgFile에 최신화 함
+ */
+ const handleFileInputChange = (e: React.ChangeEvent) => {
+ const currentImgFiles = e.target.files;
+
+ if (!currentImgFiles) return;
+
+ setCurrentImgFile(currentImgFiles[0]);
+ };
+
+ // useEffect를 이용해서 file을 preview 이미지로 생성
+ React.useEffect(() => {
+ if (!currentImgFile) return;
+
+ const previewImgUrl = URL.createObjectURL(currentImgFile);
+ setPreviewImg(previewImgUrl);
+
+ return () => URL.revokeObjectURL(previewImgUrl);
+ }, [currentImgFile]);
+
+ /**
+ * preview img의 이미지 제거 버튼 클릭시 동작하는 핸들러
+ */
+ const handleDelBtnClick = () => {
+ setPreviewImg(null);
+ setCurrentImgFile(null);
+ };
+
+ /**
+ * submit 버튼을 눌렀을때 동작하는 이벤트 핸들러
+ */
+ const handleFormSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ // 현재 이미지 파일 (currentImgFile) 값이 있는 경우 file을 url로 변경해주는 api 요청
+ // 이후에 받아온 url을 articleValues의 image 값으로 함
+ if (currentImgFile) {
+ try {
+ const imgUrl = await uploadImage({ imageFile: currentImgFile });
+ setArticleValues((prevArticleValues) => ({ ...prevArticleValues, image: imgUrl }));
+ } catch (error) {
+ console.log(error);
+ return;
+ }
+ }
+
+ // 받아온 이미지 url및 text input 값을 이용해서 게시글 업로드 요청
+ const { image, content, title } = articleValues;
+
+ try {
+ const postedArticle = await postArticle({ image, content, title });
+ if (postedArticle) {
+ const { id } = postedArticle;
+ window.location.href = `/boards/${id}`;
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ return (
+ <>
+
+
+ >
+ );
}
diff --git a/src/pages/boards/index.tsx b/src/pages/boards/index.tsx
index fec75e963..93d3be7d2 100644
--- a/src/pages/boards/index.tsx
+++ b/src/pages/boards/index.tsx
@@ -5,7 +5,7 @@ import PostList from "@/components/boards/PostList";
export default function Boards() {
return (
<>
-
+
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 6d1d3f02c..885506ac0 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,13 +1,17 @@
+import GlobalNavBar from "@/components/@shared/GlobalNavBar";
import Link from "next/link";
export default function Home() {
return (
-
- 자유게시판
-
- 게시글작성
-
- 게시글세부사항
-
+ <>
+
+
+ 자유게시판
+
+ 게시글작성
+
+ 게시글세부사항
+
+ >
);
}
diff --git a/src/pages/login.tsx b/src/pages/login.tsx
new file mode 100644
index 000000000..95933c9ff
--- /dev/null
+++ b/src/pages/login.tsx
@@ -0,0 +1,48 @@
+import Link from "next/link";
+import React from "react";
+import { logInUser, LogInUserProps } from "@/axios/auth";
+
+export default function LogIn() {
+ const [logInValues, setLogInValues] = React.useState({ email: "", password: "" });
+
+ const handleFormKeyDown = (e: React.KeyboardEvent) => {
+ if (logInValues.email === "" || logInValues.password === "") {
+ if (e.key === "Enter") e.preventDefault();
+ }
+ };
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const currentTargetId = e.target.id;
+ const currentValue = e.target.value;
+
+ setLogInValues((prevLogInValues) => ({ ...prevLogInValues, [currentTargetId]: currentValue }));
+ };
+
+ const handleFormSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const submitLogInForm = async () => {
+ await logInUser(logInValues);
+ };
+
+ submitLogInForm();
+ };
+
+ return (
+ <>
+ 로그인 페이지
+
+ 회원가입하러가기
+ >
+ );
+}
diff --git a/src/pages/signup.tsx b/src/pages/signup.tsx
new file mode 100644
index 000000000..2ed19604f
--- /dev/null
+++ b/src/pages/signup.tsx
@@ -0,0 +1,62 @@
+import Link from "next/link";
+import React from "react";
+import { signUpUser, SignUpUserProps } from "@/axios/auth";
+
+export default function SignUp() {
+ const [signUpValues, setSignUpValues] = React.useState({
+ email: "",
+ nickname: "",
+ password: "",
+ passwordConfirmation: "",
+ });
+
+ const handleFormKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") e.preventDefault();
+ };
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const currentTargetId = e.target.id;
+ const currentValue = e.target.value;
+
+ setSignUpValues((prevSignUpValues) => ({
+ ...prevSignUpValues,
+ [currentTargetId]: currentValue,
+ }));
+ };
+
+ const handleSignUpUserSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const submitSignUpForm = async () => {
+ await signUpUser(signUpValues);
+ };
+
+ submitSignUpForm();
+ };
+
+ return (
+ <>
+ 회원가입 페이지
+
+ 로그인하러가기
+ >
+ );
+}
diff --git a/src/styles/global.css b/src/styles/global.css
index 05a774a58..4aa73a3c1 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -2,10 +2,7 @@
@tailwind components;
@tailwind utilities;
-@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-dynamic-subset.min.css");
-
@layer base {
- *,
html {
font-family: "Pretendard Variable", "Noto Sans KR", sans-serif;
}
@@ -14,4 +11,8 @@
button {
cursor: pointer;
}
+
+ textarea {
+ resize: none;
+ }
}
diff --git a/src/utils/elapsedTimeCalc.ts b/src/utils/elapsedTimeCalc.ts
new file mode 100644
index 000000000..5e5e8d003
--- /dev/null
+++ b/src/utils/elapsedTimeCalc.ts
@@ -0,0 +1,28 @@
+/**
+ * string 형태의 date를 받아서 현재로 부터 경과된 시간이 얼마나 되는지 string으로 return하는 함수
+ */
+const elapsedTimeCalc = (comparedDate: string) => {
+ const currentDate = new Date();
+ const specificDate = new Date(comparedDate);
+
+ const timeDifference = currentDate.getTime() - specificDate.getTime();
+
+ const min = 1000 * 60;
+ const hour = min * 60;
+ const day = hour * 24;
+ const week = day * 7;
+
+ if (timeDifference >= week) {
+ return `${Math.floor(timeDifference / week)}주 전`;
+ } else if (timeDifference >= day) {
+ return `${Math.floor(timeDifference / day)}일 전`;
+ } else if (timeDifference >= hour) {
+ return `${Math.floor(timeDifference / hour)}시간 전`;
+ } else if (timeDifference >= min) {
+ return `${Math.floor(timeDifference / min)}분 전`;
+ } else {
+ return `방금 전`;
+ }
+};
+
+export default elapsedTimeCalc;
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 8cb801e58..919c4bab6 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -31,6 +31,7 @@ const config: Config = {
"blue-hover": "#1967d6",
"blue-active": "#1251aa",
"header-under": "#dfdfdf",
+ "comment-bg": "#fcfcfc",
},
},
},