+
+
베스트 게시글
{bestArticles.map((article, index) => (
-
+
+
+
))}
@@ -31,9 +34,12 @@ export default function Boards() {
게시글
-
+
{articleResults?.map((article, index) => (
-
+
+
+
))}
- {loading ?? (
-
-
-
- )}
+ {loading ??
}
);
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 696eae13..df1f50a1 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -17,7 +17,7 @@ export default function RootLayout({
-
+
{children}
diff --git a/src/components/AddComment.tsx b/src/components/AddComment.tsx
new file mode 100644
index 00000000..0309bd74
--- /dev/null
+++ b/src/components/AddComment.tsx
@@ -0,0 +1,48 @@
+"use client";
+import React, { useEffect, useState } from "react";
+
+type Props = {};
+
+const AddComment = (props: Props) => {
+ const [comment, setComment] = useState("");
+ const [disable, setDisable] = useState(true);
+ const [addButtonColor, setAddButtonColor] = useState("#9CA3AF");
+
+ const handleChange = (e: React.ChangeEvent
) => {
+ setComment(e.target.value);
+ };
+
+ useEffect(() => {
+ if (comment === "") {
+ setDisable(true);
+ setAddButtonColor("#9CA3AF");
+ } else {
+ setDisable(false);
+ setAddButtonColor("#3692FF");
+ }
+ }, [comment]);
+
+ return (
+
+ );
+};
+export default AddComment;
diff --git a/src/components/AddImage.tsx b/src/components/AddImage.tsx
new file mode 100644
index 00000000..8d1b0a8b
--- /dev/null
+++ b/src/components/AddImage.tsx
@@ -0,0 +1,68 @@
+"use client";
+import { IoIosClose } from "react-icons/io";
+import React, { useEffect, useState } from "react";
+import { GoPlus } from "react-icons/go";
+
+type Props = {};
+
+const AddImage = (props: Props) => {
+ const [preview, setPreview] = useState(null);
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ const fileURL = URL.createObjectURL(file);
+ setPreview(fileURL);
+ }
+ };
+
+ const handleCancel = () => {
+ setPreview(null);
+ if (preview) {
+ URL.revokeObjectURL(preview);
+ }
+ };
+
+ return (
+
+
이미지
+
+
+
+ {preview && (
+
+

+
+
+ )}
+
+
+
+
+ );
+};
+
+export default AddImage;
diff --git a/src/components/Article.tsx b/src/components/Article.tsx
index a55f79f6..f05b23d5 100644
--- a/src/components/Article.tsx
+++ b/src/components/Article.tsx
@@ -1,24 +1,13 @@
+import { Articles } from "@/types/article";
import Image from "next/image";
import React from "react";
import { IoMdHeartEmpty } from "react-icons/io";
-interface Articles {
- article: {
- id: number;
- title: string;
- content: string;
- image: string;
- likeCount: number;
- createdAt: string;
- updatedAt: string;
- writer: {
- nickname: string;
- id: number;
- };
- };
+interface Article {
+ article: Articles;
}
-const Article = ({ article }: Articles) => {
+const Article = ({ article }: Article) => {
const formattedDate = new Date(article.createdAt)
.toLocaleDateString("ko-KR", {
year: "numeric",
diff --git a/src/components/BestArticle.tsx b/src/components/BestArticle.tsx
index fc973b51..f54dea98 100644
--- a/src/components/BestArticle.tsx
+++ b/src/components/BestArticle.tsx
@@ -1,31 +1,20 @@
+import { Articles } from "@/types/article";
import Image from "next/image";
import React from "react";
import { IoMdHeartEmpty } from "react-icons/io";
type Props = {
- article: {
- id: number;
- title: string;
- content: string;
- image: string;
- likeCount: number;
- createdAt: string;
- updatedAt: string;
- writer: {
- nickname: string;
- id: number;
- };
- };
+ article: Articles;
};
const BestArticle = ({ article }: Props) => {
- const formattedDate = new Date(article.createdAt)
- .toLocaleDateString("ko-KR", {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- })
- .replace(/\.$/, "");
+ const formattedDate = new Date(article.createdAt)
+ .toLocaleDateString("ko-KR", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ })
+ .replace(/\.$/, "");
return (
diff --git a/src/components/Comment.tsx b/src/components/Comment.tsx
new file mode 100644
index 00000000..a1571b9e
--- /dev/null
+++ b/src/components/Comment.tsx
@@ -0,0 +1,47 @@
+import { CommentType } from "@/types/comment";
+import Image from "next/image";
+import React from "react";
+import { BsThreeDotsVertical } from "react-icons/bs";
+
+type Props = {
+ comment: CommentType;
+};
+
+const Comment = ({ comment }: Props) => {
+ const formattedDateForComment = comment?.createdAt
+ ? new Date(comment.createdAt)
+ .toLocaleDateString("ko-KR", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ })
+ .replace(/\.$/, "")
+ : "";
+
+ return (
+
+
+
+ {comment.content}
+
+
+
+
+
+
+
+
+
+
+ {comment.writer.nickname}
+
+
+ {formattedDateForComment}
+
+
+
+
+ );
+};
+
+export default Comment;
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 03ed38b0..a51c49b6 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -1,22 +1,27 @@
import Image from "next/image";
+import Link from "next/link";
import React from "react";
const Header = () => {
return (
<>
-
+
-
+
+
+
-
- 판다마켓
-
+
+
+ 판다마켓
+
+
자유게시판
diff --git a/src/components/InputText.tsx b/src/components/InputText.tsx
new file mode 100644
index 00000000..abf3cfef
--- /dev/null
+++ b/src/components/InputText.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+
+type Props = {
+ setTitle: React.Dispatch
>;
+};
+
+const InputText = ({ setTitle }: Props) => {
+ const handleChange = (e: React.ChangeEvent) => {
+ setTitle(e.target.value);
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+export default InputText;
diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx
new file mode 100644
index 00000000..a80c7e7d
--- /dev/null
+++ b/src/components/Loading.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+import { motion } from "framer-motion";
+
+type Props = {};
+
+const Loading = (props: Props) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default Loading;
diff --git a/src/components/TextArea.tsx b/src/components/TextArea.tsx
new file mode 100644
index 00000000..ef76eb3c
--- /dev/null
+++ b/src/components/TextArea.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+
+type Props = {
+ setContent: React.Dispatch>;
+};
+
+const TextArea = ({ setContent }: Props) => {
+ const handleChange = (e: React.ChangeEvent) => {
+ setContent(e.target.value);
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+export default TextArea;
diff --git a/src/hooks/useArticle.ts b/src/hooks/useArticle.ts
index 7f0abba0..61ca0ea4 100644
--- a/src/hooks/useArticle.ts
+++ b/src/hooks/useArticle.ts
@@ -1,20 +1,7 @@
import getArticles from "@/services/getArticles";
+import { Articles } from "@/types/article";
import { useCallback, useEffect, useState } from "react";
-export interface Articles {
- id: number;
- title: string;
- content: string;
- image: string;
- likeCount: number;
- createdAt: string;
- updatedAt: string;
- writer: {
- nickname: string;
- id: number;
- };
-}
-
interface Props {
setArticleResults: React.Dispatch>;
basis: string;
diff --git a/src/hooks/useArticleDetails.ts b/src/hooks/useArticleDetails.ts
new file mode 100644
index 00000000..b9912c71
--- /dev/null
+++ b/src/hooks/useArticleDetails.ts
@@ -0,0 +1,28 @@
+import getArticleDetails from "@/services/getArticleDetails";
+import { Articles } from "@/types/article";
+import React, { useEffect, useState } from "react";
+
+const useArticleDetails = (id: string) => {
+ const [articleDetails, setArticleDetails] = useState(null);
+
+ useEffect(() => {
+ const fetchDetail = async () => {
+ try {
+ const res = await getArticleDetails(id);
+ setArticleDetails(res ?? null);
+ } catch (error: any) {
+ if (error.response) {
+ throw new Error("게시글 상세정보 불러오기 실패");
+ }
+ }
+ };
+
+ fetchDetail();
+ }, []);
+
+ return {
+ articleDetails,
+ };
+};
+
+export default useArticleDetails;
diff --git a/src/hooks/useBestArticle.ts b/src/hooks/useBestArticle.ts
index c6cbd0dc..e76752e1 100644
--- a/src/hooks/useBestArticle.ts
+++ b/src/hooks/useBestArticle.ts
@@ -1,20 +1,7 @@
import getArticles from "@/services/getArticles";
+import { Articles } from "@/types/article";
import { useEffect, useState } from "react";
-interface Articles {
- id: number;
- title: string;
- content: string;
- image: string;
- likeCount: number;
- createdAt: string;
- updatedAt: string;
- writer: {
- nickname: string;
- id: number;
- };
-}
-
const useBestArticle = () => {
const [bestArticles, setArticles] = useState([]);
const [windowSize, setWindowSize] = useState(0);
diff --git a/src/hooks/useComment.ts b/src/hooks/useComment.ts
new file mode 100644
index 00000000..34be1358
--- /dev/null
+++ b/src/hooks/useComment.ts
@@ -0,0 +1,36 @@
+import getComment from "@/services/getComment";
+import { CommentType } from "@/types/comment";
+import React, { useEffect, useState } from "react";
+
+type Props = {
+ id: string;
+};
+
+const useComment = ({ id }: Props) => {
+ const [comments, setComment] = useState([]);
+ const [nextCursor, setNextCursor] = useState(null);
+
+ useEffect(() => {
+ const fetchComment = async () => {
+ try {
+ const res = await getComment({ id, limit: 10 });
+ setComment(res?.list ?? []);
+ setNextCursor(res?.nextCursor ?? null);
+ } catch (error: any) {
+ if (error.response) {
+ throw new Error("comment 가져오기 실패");
+ }
+ }
+ };
+ fetchComment();
+ }, []);
+
+ return {
+ comments,
+ nextCursor,
+ setComment,
+ setNextCursor,
+ };
+};
+
+export default useComment;
diff --git a/src/hooks/useScroll.ts b/src/hooks/useScroll.ts
new file mode 100644
index 00000000..853852a2
--- /dev/null
+++ b/src/hooks/useScroll.ts
@@ -0,0 +1,54 @@
+import React, { useCallback, useEffect, useState } from "react";
+import getComment from "@/services/getComment";
+import { CommentType } from "@/types/comment";
+
+type Props = {
+ nextCursor: number | null;
+ setComment: React.Dispatch>;
+ id: string;
+ setNextCursor: React.Dispatch>;
+};
+
+const useScroll = ({ nextCursor, setComment, id, setNextCursor }: Props) => {
+ const [loading, setLoading] = useState(false);
+
+ const handleScroll = useCallback(() => {
+ if (
+ window.innerHeight + window.scrollY >=
+ document.documentElement.scrollHeight - 30
+ ) {
+ setLoading(true);
+ } else {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ window.addEventListener("scroll", handleScroll);
+
+ return () => {
+ window.removeEventListener("scroll", handleScroll);
+ };
+ }, [handleScroll]);
+
+ useEffect(() => {
+ const fetchMoreComment = async () => {
+ if (loading && nextCursor !== null) {
+ const res = await getComment({ id, limit: 10, cursor: nextCursor });
+ if (res?.list) {
+ console.log(`nextCursor: ${res.nextCursor}`);
+ setComment((prev) => [...prev, ...res.list]);
+ setNextCursor(res.nextCursor);
+ }
+ }
+ };
+
+ fetchMoreComment();
+ }, [loading]);
+
+ return {
+ loading,
+ };
+};
+
+export default useScroll;
diff --git a/src/services/getArticleDetails.ts b/src/services/getArticleDetails.ts
new file mode 100644
index 00000000..cc1748df
--- /dev/null
+++ b/src/services/getArticleDetails.ts
@@ -0,0 +1,30 @@
+import axios from "axios";
+
+const Base_URL = "https://panda-market-api.vercel.app";
+
+interface Article {
+ id: number;
+ title: string;
+ content: string;
+ image: string;
+ likeCount: number;
+ createdAt: string;
+ updatedAt: string;
+ writer: {
+ nickname: string;
+ id: number;
+ };
+}
+
+export default async function getArticleDetails(id: string) {
+ try {
+ const response = await axios.get(`${Base_URL}/articles/${id}`);
+ console.log("success");
+ console.log(response);
+ return response.data ?? null;
+ } catch (error: any) {
+ if (error.response) {
+ throw new Error("게시글 불러오기 실패");
+ }
+ }
+}
diff --git a/src/services/getComment.ts b/src/services/getComment.ts
new file mode 100644
index 00000000..a5e45b98
--- /dev/null
+++ b/src/services/getComment.ts
@@ -0,0 +1,47 @@
+import axios from "axios";
+
+const Base_URL = "https://panda-market-api.vercel.app";
+
+interface Comment {
+ nextCursor: number;
+ list: {
+ id: number;
+ title: string;
+ content: string;
+ createdAt: string;
+ updatedAt: string;
+ writer: {
+ image: string;
+ nickname: string;
+ id: number;
+ };
+ }[];
+}
+
+interface Props {
+ id: string;
+ limit: number;
+ cursor?: number | null;
+}
+
+export default async function getComment({ id, limit, cursor }: Props) {
+ try {
+ let query;
+ if (cursor) {
+ query = `limit=${limit}&cursor=${cursor}`;
+ } else {
+ query = `limit=${limit}`;
+ }
+
+ const response = await axios.get(
+ `${Base_URL}/articles/${id}/comments?${query}`
+ );
+ console.log("comment connection successful!");
+ console.log(response);
+ return response.data ?? null;
+ } catch (error: any) {
+ if (error.response) {
+ throw new Error("게시글 불러오기 실패");
+ }
+ }
+}
diff --git a/src/types/article.ts b/src/types/article.ts
new file mode 100644
index 00000000..2a6a6231
--- /dev/null
+++ b/src/types/article.ts
@@ -0,0 +1,13 @@
+export interface Articles {
+ id: number;
+ title: string;
+ content: string;
+ image: string;
+ likeCount: number;
+ createdAt: string;
+ updatedAt: string;
+ writer: {
+ nickname: string;
+ id: number;
+ };
+}
diff --git a/src/types/comment.ts b/src/types/comment.ts
new file mode 100644
index 00000000..71d4cd17
--- /dev/null
+++ b/src/types/comment.ts
@@ -0,0 +1,12 @@
+export interface CommentType {
+ id: number;
+ title: string;
+ content: string;
+ createdAt: string;
+ updatedAt: string;
+ writer: {
+ image: string;
+ nickname: string;
+ id: number;
+ };
+}