diff --git a/package-lock.json b/package-lock.json
index 6529ebf9e..a2a4a56dc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@svgr/webpack": "^8.1.0",
+ "dayjs": "^1.11.13",
"next": "14.2.23",
"react": "^18",
"react-dom": "^18",
@@ -3003,6 +3004,7 @@
"integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==",
"license": "MIT",
"dependencies": {
+
"@babel/helper-define-polyfill-provider": "^0.6.3"
},
"peerDependencies": {
@@ -3447,6 +3449,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/dayjs": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
+ "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
diff --git a/package.json b/package.json
index 190559fd3..3355b8b05 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
},
"dependencies": {
"@svgr/webpack": "^8.1.0",
+ "dayjs": "^1.11.13",
"next": "14.2.23",
"react": "^18",
"react-dom": "^18",
diff --git a/public/assets/img/comment_empty.png b/public/assets/img/comment_empty.png
new file mode 100644
index 000000000..20a15d28f
Binary files /dev/null and b/public/assets/img/comment_empty.png differ
diff --git a/public/assets/img/icon_add.svg b/public/assets/img/icon_add.svg
new file mode 100644
index 000000000..4d8a961a1
--- /dev/null
+++ b/public/assets/img/icon_add.svg
@@ -0,0 +1,6 @@
+
diff --git a/public/assets/img/icon_back.svg b/public/assets/img/icon_back.svg
new file mode 100644
index 000000000..9ba5ad945
--- /dev/null
+++ b/public/assets/img/icon_back.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/assets/img/icon_close.svg b/public/assets/img/icon_close.svg
new file mode 100644
index 000000000..a85baceda
--- /dev/null
+++ b/public/assets/img/icon_close.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/assets/img/icon_dot.svg b/public/assets/img/icon_dot.svg
new file mode 100644
index 000000000..63a0344c3
--- /dev/null
+++ b/public/assets/img/icon_dot.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/assets/img/icon_wish_active.svg b/public/assets/img/icon_wish_active.svg
new file mode 100644
index 000000000..91aded985
--- /dev/null
+++ b/public/assets/img/icon_wish_active.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/board-best-list.module.css b/src/components/board-best-list.module.css
index 003a50e7d..3a45d1102 100644
--- a/src/components/board-best-list.module.css
+++ b/src/components/board-best-list.module.css
@@ -53,6 +53,7 @@
line-clamp: 2;
}
.board_best_box .content figure {
+ overflow: hidden;
display: block;
width: 70px;
height: 70px;
diff --git a/src/components/board-best-list.tsx b/src/components/board-best-list.tsx
index 8350d813e..aa4b98801 100644
--- a/src/components/board-best-list.tsx
+++ b/src/components/board-best-list.tsx
@@ -6,7 +6,7 @@ import formatDate from "@/lib/format-data";
export default function BoardBestList(data: Article) {
return (
-
+

Best
diff --git a/src/components/board-comment.module.css b/src/components/board-comment.module.css
new file mode 100644
index 000000000..3e5cb180d
--- /dev/null
+++ b/src/components/board-comment.module.css
@@ -0,0 +1,41 @@
+.comment {
+ padding-right: 30px;
+ position: relative;
+ padding-bottom: 12px;
+ margin-bottom: 24px;
+ border-bottom: 1px solid #e5e7eb;
+}
+.comment > h3 {
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 1.7142;
+ margin-bottom: 24px;
+}
+.comment > .info {
+ display: flex;
+}
+.comment > .info > figure {
+ overflow: hidden;
+ display: block;
+ margin-right: 8px;
+ width: 32px;
+ height: 32px;
+ border-radius: 9999px;
+}
+.comment > .info > figure > img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+.comment > .info > .right {
+ font-size: 12px;
+ line-height: 1.5;
+}
+.comment > .info > .right > p {
+ color: #4b5563;
+ margin-bottom: 4px;
+}
+.comment > .info > .right > span {
+ display: block;
+ color: #9ca3af;
+}
diff --git a/src/components/board-comment.tsx b/src/components/board-comment.tsx
new file mode 100644
index 000000000..15b94d8a4
--- /dev/null
+++ b/src/components/board-comment.tsx
@@ -0,0 +1,37 @@
+import formatDateNow from "@/lib/format-data-now";
+import ToggleMenu from "@/components/toggle-menu";
+import styles from "./board-comment.module.css";
+import { Comment } from "../../types";
+
+/**
+ * 질문하기
+ * Comment 인터페이스의 타입을 전부 사용안했는데 오류가 나지않는 이유??
+ * 예를 들어 createdAt는 ?. 처럼 선택적이 아니지만 오류가 안남
+ */
+
+const BoardComment = (props: Comment) => {
+ return (
+
+
+ {props.content}
+
+
+
+
+
+
{props.writer.nickname}
+
{formatDateNow(props.updatedAt)}
+
+
+
+ );
+};
+
+export default BoardComment;
diff --git a/src/components/board-list.module.css b/src/components/board-list.module.css
index 4b5990c56..c9e85e90c 100644
--- a/src/components/board-list.module.css
+++ b/src/components/board-list.module.css
@@ -50,6 +50,7 @@
flex-direction: column;
}
.board_common_box .right figure {
+ overflow: hidden;
display: block;
margin-bottom: 16px;
margin-left: auto;
diff --git a/src/components/board-list.tsx b/src/components/board-list.tsx
index 7a522c4eb..21c64b7c4 100644
--- a/src/components/board-list.tsx
+++ b/src/components/board-list.tsx
@@ -6,7 +6,7 @@ import formatDate from "@/lib/format-data";
export default function BoardList(data: Article) {
return (
-
+
{data.title}
diff --git a/src/components/form/form-input.tsx b/src/components/form/form-input.tsx
new file mode 100644
index 000000000..b2e35890e
--- /dev/null
+++ b/src/components/form/form-input.tsx
@@ -0,0 +1,22 @@
+import styles from "./form.module.css";
+
+interface Props {
+ type: string;
+ placeholder: string;
+ value?: string;
+ onChange: (e: React.ChangeEvent
) => void;
+}
+
+const FormInput = ({ type, placeholder, value, onChange }: Props) => {
+ return (
+
+ );
+};
+
+export default FormInput;
diff --git a/src/components/form/form-textarea.tsx b/src/components/form/form-textarea.tsx
new file mode 100644
index 000000000..90fd4c3cf
--- /dev/null
+++ b/src/components/form/form-textarea.tsx
@@ -0,0 +1,21 @@
+import styles from "./form.module.css";
+
+interface Props {
+ placeholder: string;
+ height?: number;
+ value?: string;
+ onChange: (e: React.ChangeEvent) => void;
+}
+
+const FormTextarea = ({ placeholder, height, value, onChange }: Props) => {
+ return (
+
+ );
+};
+
+export default FormTextarea;
diff --git a/src/components/form/form.module.css b/src/components/form/form.module.css
new file mode 100644
index 000000000..7045ae6c8
--- /dev/null
+++ b/src/components/form/form.module.css
@@ -0,0 +1,52 @@
+.input {
+ display: block;
+ background: inherit;
+ border: none;
+ outline: none;
+ box-shadow: none;
+ border-radius: 0;
+ padding: 0;
+ overflow: visible;
+}
+.input {
+ width: 100%;
+ background-color: #f3f4f6;
+ font-size: 16px;
+ padding: 0 24px;
+ border-radius: 12px;
+ height: 56px;
+ display: flex;
+ align-items: center;
+}
+.input::placeholder {
+ color: #9ca3af;
+}
+
+.textarea {
+ display: block;
+ background: inherit;
+ border: none;
+ outline: none;
+ box-shadow: none;
+ border-radius: 0;
+ padding: 0;
+ overflow: visible;
+ resize: none;
+}
+.textarea {
+ width: 100%;
+ background-color: #f3f4f6;
+ font-size: 16px;
+ padding: 16px 24px;
+ border-radius: 12px;
+ height: 282px;
+}
+.textarea::placeholder {
+ color: #9ca3af;
+}
+
+@media (max-width: 744px) {
+ .textarea {
+ height: 200px;
+ }
+}
diff --git a/src/components/toggle-menu.tsx b/src/components/toggle-menu.tsx
new file mode 100644
index 000000000..630bfdd1b
--- /dev/null
+++ b/src/components/toggle-menu.tsx
@@ -0,0 +1,31 @@
+import { useState } from "react";
+import styles from "./toggle.menu.module.css";
+import { useOutsideClick } from "@/hooks/useOutsideClick";
+
+const ToggleMenu = () => {
+ const [menu, setMenu] = useState(false);
+
+ const onMenuToggle = () => {
+ setMenu(!menu);
+ };
+
+ const ref = useOutsideClick(() => {
+ setMenu(false);
+ });
+
+ return (
+
+
+
+
+ {menu && (
+
+ )}
+
+ );
+};
+
+export default ToggleMenu;
diff --git a/src/components/toggle.menu.module.css b/src/components/toggle.menu.module.css
new file mode 100644
index 000000000..2fe00381e
--- /dev/null
+++ b/src/components/toggle.menu.module.css
@@ -0,0 +1,25 @@
+.menu {
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+.menu > p {
+ cursor: pointer;
+}
+.menu > ul {
+ position: absolute;
+ top: calc(100% + 5px);
+ right: 0;
+ z-index: 10;
+ width: 100px;
+ text-align: center;
+ border: 1px solid #e5e7eb;
+ background-color: #fff;
+ padding: 5px 0;
+ border-radius: 12px;
+}
+.menu > ul > li {
+ padding: 5px 0;
+ font-size: 14px;
+ cursor: pointer;
+}
diff --git a/src/hooks/useMediaQuery.tsx b/src/hooks/useMediaQuery.tsx
index a016b55d7..0d922d7b5 100644
--- a/src/hooks/useMediaQuery.tsx
+++ b/src/hooks/useMediaQuery.tsx
@@ -1,30 +1,3 @@
-/* isMount 안쓰고 하는방법.. 근데 안됨
-import { useEffect, useState } from "react";
-import { useMediaQuery } from "react-responsive";
-
-export const useIsMo = () => {
- const [isMobile, setIsMobile] = useState(false);
- const mobile = useMediaQuery({ query: "(max-width: 744px)" });
-
- useEffect(() => {
- setIsMobile(mobile);
- }, [mobile]);
-
- return isMobile;
-};
-
-export const useIsTa = () => {
- const [isTablet, setIsTablet] = useState(false);
- const tablet = useMediaQuery({ query: "(max-width: 1200px)" });
-
- useEffect(() => {
- setIsTablet(tablet);
- }, [tablet]);
-
- return isTablet;
-};
-*/
-
import { useMediaQuery } from "react-responsive";
export const useIsMo = () => {
diff --git a/src/lib/fetch-board.ts b/src/lib/fetch-board.ts
new file mode 100644
index 000000000..14fe10e2f
--- /dev/null
+++ b/src/lib/fetch-board.ts
@@ -0,0 +1,17 @@
+import { Review } from "../../types";
+
+const fetchBoard = async (id: number): Promise => {
+ const url = `https://panda-market-api.vercel.app/articles/${id}`;
+ try {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error();
+ }
+ return await response.json();
+ } catch (err) {
+ console.error(err);
+ return null;
+ }
+};
+
+export default fetchBoard;
diff --git a/src/lib/fetch-comments.ts b/src/lib/fetch-comments.ts
new file mode 100644
index 000000000..447b8ee65
--- /dev/null
+++ b/src/lib/fetch-comments.ts
@@ -0,0 +1,19 @@
+import { CommentListResponse } from "../../types";
+
+const fetchComments = async (
+ id: number
+): Promise => {
+ const url = `https://panda-market-api.vercel.app/articles/${id}/comments?limit=100`;
+ try {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error();
+ }
+ return await response.json();
+ } catch (err) {
+ console.error(err);
+ return null;
+ }
+};
+
+export default fetchComments;
diff --git a/src/lib/format-data-now.ts b/src/lib/format-data-now.ts
new file mode 100644
index 000000000..3200655e4
--- /dev/null
+++ b/src/lib/format-data-now.ts
@@ -0,0 +1,10 @@
+import dayjs from "dayjs";
+import relativeTime from "dayjs/plugin/relativeTime";
+import "dayjs/locale/ko";
+
+dayjs.extend(relativeTime);
+dayjs.locale("ko");
+
+export default function formatDateNow(isoDate: string) {
+ return dayjs(isoDate).fromNow();
+}
diff --git a/src/lib/format-data.ts b/src/lib/format-data.ts
index d35bf581f..97d6fc2a6 100644
--- a/src/lib/format-data.ts
+++ b/src/lib/format-data.ts
@@ -1,8 +1,8 @@
+import dayjs from "dayjs";
+
export default function formatDate(dateString: string) {
- const date = new Date(dateString);
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, "0");
- const day = String(date.getDate()).padStart(2, "0");
+ const isoDate = new Date(dateString);
+ const formattedDate = dayjs(isoDate).format("YYYY. MM. DD");
- return `${year}. ${month}. ${day}`;
+ return formattedDate;
}
diff --git a/src/pages/addboard/addboard.module.css b/src/pages/addboard/addboard.module.css
new file mode 100644
index 000000000..7f426afca
--- /dev/null
+++ b/src/pages/addboard/addboard.module.css
@@ -0,0 +1,80 @@
+.add_common_title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24px;
+}
+.add_common_title > div {
+ margin: 0;
+}
+.form_list > li + li {
+ margin-top: 24px;
+}
+.form_tit {
+ margin-bottom: 12px;
+ font-size: 18px;
+ font-weight: 700;
+ line-height: 1.44;
+}
+.form_tit.req::before {
+ content: "*";
+}
+.form_file input[type="file"] {
+ display: none;
+}
+.form_file input[type="file"] + label {
+ width: 282px;
+ height: 282px;
+ background-color: #f3f4f6;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ border-radius: 12px;
+ cursor: pointer;
+}
+.form_file input[type="file"] + label span {
+ font-size: 16px;
+ color: #9ca3af;
+ margin-top: 12px;
+}
+
+.form_file .img_box {
+ position: relative;
+ width: 282px;
+ height: 0;
+ padding-top: 282px;
+ overflow: hidden;
+}
+.form_file .img_box .close {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ z-index: 3;
+ cursor: pointer;
+}
+.form_file .img_box > img {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 12px;
+}
+
+@media (max-width: 1200px) {
+ .form_file input[type="file"] + label {
+ width: 168px;
+ height: 168px;
+ }
+ .form_file .img_box {
+ width: 168px;
+ padding-top: 168px;
+ }
+}
+@media (max-width: 744px) {
+ .form_tit {
+ font-size: 14px;
+ }
+}
diff --git a/src/pages/addboard/index.tsx b/src/pages/addboard/index.tsx
new file mode 100644
index 000000000..7bfe0df2b
--- /dev/null
+++ b/src/pages/addboard/index.tsx
@@ -0,0 +1,116 @@
+import { useState, useRef, useEffect } from "react";
+import styles from "./addboard.module.css";
+import FormInput from "@/components/form/form-input";
+import FormTextarea from "@/components/form/form-textarea";
+
+export default function Page() {
+ const [title, setTitle] = useState("");
+ const [content, setContent] = useState("");
+ const [submitState, setSubmitState] = useState(true);
+
+ const [imageSrc, setImageSrc] = useState("");
+ const fileRef = useRef(null);
+
+ const onChangeTitle: React.ChangeEventHandler = (e) => {
+ setTitle(e.target.value);
+ };
+
+ const onChangeContent: React.ChangeEventHandler = (
+ e
+ ) => {
+ setContent(e.target.value);
+ };
+
+ const encodeFileToBase64: React.ChangeEventHandler = (
+ e
+ ) => {
+ if (e.target.files) {
+ const fileBlob = e.target.files[0];
+ const reader = new FileReader();
+
+ reader.onload = () => {
+ if (reader.result) {
+ setImageSrc(reader.result as string);
+ }
+ };
+
+ reader.readAsDataURL(fileBlob);
+ }
+ };
+
+ const onFileClose: React.MouseEventHandler = () => {
+ setImageSrc("");
+ if (fileRef.current && fileRef.current.value) {
+ fileRef.current.value = "";
+ }
+ };
+
+ useEffect(() => {
+ if (title !== "" && content !== "") {
+ setSubmitState(false);
+ } else {
+ setSubmitState(true);
+ }
+ }, [title, content]);
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/pages/board/[id].tsx b/src/pages/board/[id].tsx
new file mode 100644
index 000000000..59c803f40
--- /dev/null
+++ b/src/pages/board/[id].tsx
@@ -0,0 +1,113 @@
+import styles from "./board.module.css";
+import FormTextarea from "@/components/form/form-textarea";
+import Link from "next/link";
+import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
+import fetchBoard from "@/lib/fetch-board";
+import formatDate from "@/lib/format-data";
+import fetchComments from "@/lib/fetch-comments";
+import ToggleMenu from "@/components/toggle-menu";
+import BoardComment from "@/components/board-comment";
+import { useEffect, useState } from "react";
+
+export const getServerSideProps = async (
+ context: GetServerSidePropsContext
+) => {
+ const id = context.params!.id;
+ const data = await fetchBoard(Number(id));
+ const comment = await fetchComments(Number(id));
+ return {
+ props: {
+ data,
+ comment: comment?.list,
+ },
+ };
+};
+
+export default function Page({
+ data,
+ comment,
+}: InferGetServerSidePropsType) {
+ // null 일수도 있어서 예외처리를 해줘야 아래 data 사용 시 ?. 처리 안해줘도 됨.
+ if (!data) return "문제가 발생했습니다. 다시 시도해주세요.";
+
+ const [commentContent, setCommentContent] = useState("");
+ const [commentState, setCommentState] = useState(true);
+
+ const onChangeComment: React.ChangeEventHandler = (
+ e
+ ) => {
+ setCommentContent(e.target.value);
+ };
+
+ useEffect(() => {
+ if (commentContent !== "") {
+ setCommentState(false);
+ } else {
+ setCommentState(true);
+ }
+ }, [commentContent]);
+
+ return (
+ <>
+
+
+
+ {data.title}
+
+
+
+
+
+
+
{data.writer.nickname}
+
{formatDate(data.updatedAt)}
+
+
+ {data.isLiked ? (
+

+ ) : (
+

+ )}
+
{data.likeCount}
+
+
+
+
+
+ {data.image ?

: null}
+ {data.content}
+
+
+
+
댓글달기
+
+
+
+ {comment?.length === 0 ? (
+
+

+
+ 아직 댓글이 없어요,
지금 댓글을 달아보세요!
+
+
+ ) : (
+
+ {comment?.map((el) => {
+ return ;
+ })}
+
+ )}
+
+
+ 목록으로 돌아가기
+
+
+ >
+ );
+}
diff --git a/src/pages/board/board.module.css b/src/pages/board/board.module.css
new file mode 100644
index 000000000..7c7d1bc9f
--- /dev/null
+++ b/src/pages/board/board.module.css
@@ -0,0 +1,132 @@
+.board_info {
+ position: relative;
+}
+.board_info .common_title {
+ padding-right: 30px;
+}
+.board_info .btm {
+ display: flex;
+ align-items: center;
+ padding: 0 0 16px;
+ border-bottom: 1px solid #e5e7eb;
+}
+.board_info .btm .profile {
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ padding-right: 32px;
+ margin-right: 32px;
+ border-right: 1px solid #e5e7eb;
+}
+.board_info .btm .profile > figure {
+ overflow: hidden;
+ display: block;
+ margin-right: 16px;
+ width: 40px;
+ height: 40px;
+ border-radius: 9999px;
+}
+.board_info .btm .profile > figure > img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+.board_info .btm .profile > p {
+ margin-right: 8px;
+ font-weight: 500;
+ color: #4b5563;
+}
+.board_info .btm .profile > span {
+ color: #9ca3af;
+}
+.board_info .btm .like_btn {
+ cursor: pointer;
+ padding: 4px 12px;
+ border: 1px solid #e5e7eb;
+ border-radius: 999px;
+ display: flex;
+ align-items: center;
+}
+.board_info .btm .like_btn > img {
+ margin-right: 4px;
+ height: 32px;
+}
+.board_info .btm .like_btn > span {
+ font-size: 16px;
+ font-weight: 500;
+ color: #6b7280;
+ line-height: 1;
+}
+
+.content {
+ min-height: 200px;
+ padding-top: 24px;
+ padding-bottom: 32px;
+ font-size: 18px;
+ line-height: 1.4444;
+ font-weight: 400;
+}
+.content > img {
+ display: block;
+ max-width: 100%;
+ margin-bottom: 12px;
+}
+.comment_add {
+ margin-bottom: 40px;
+}
+.comment_add > h3 {
+ font-size: 16px;
+ font-weight: 600;
+ margin-bottom: 10px;
+}
+.comment_add > button {
+ margin-top: 16px;
+ margin-left: auto;
+}
+
+.empty {
+ padding: 40px 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+}
+.empty > img {
+ height: 140px;
+}
+.empty > span {
+ font-size: 16px;
+ color: #9ca3af;
+ line-height: 1.625;
+ text-align: center;
+}
+
+.list {
+ margin-bottom: 64px;
+}
+
+.link {
+ height: 48px;
+ width: 240px;
+ margin: 0 auto;
+}
+
+@media (max-width: 744px) {
+ .board_info .btm .profile {
+ padding-right: 16px;
+ margin-right: 16px;
+ }
+ .board_info .btm .profile > p {
+ margin-right: 5px;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 1;
+ -webkit-box-orient: vertical;
+ }
+
+ .board_info .btm .like_btn > img {
+ height: 24px;
+ }
+}
diff --git a/src/pages/boards/board.module.css b/src/pages/boards/board.module.css
index ca296f571..a2081c4f9 100644
--- a/src/pages/boards/board.module.css
+++ b/src/pages/boards/board.module.css
@@ -66,6 +66,12 @@
line-height: 1;
background-color: #fff;
}
+.select_box > div > .pc {
+ display: block;
+}
+.select_box > div > .mo {
+ display: none;
+}
.select_box > ul {
position: absolute;
top: calc(100% + 5px);
@@ -102,6 +108,12 @@
padding: 0;
background: none;
}
+ .select_box > div > .pc {
+ display: none;
+ }
+ .select_box > div > .mo {
+ display: block;
+ }
.select_box > ul {
width: auto;
}
diff --git a/src/pages/boards/index.tsx b/src/pages/boards/index.tsx
index 2af5d2be6..691da7189 100644
--- a/src/pages/boards/index.tsx
+++ b/src/pages/boards/index.tsx
@@ -3,12 +3,14 @@ import styles from "./board.module.css";
import BoardBestList from "@/components/board-best-list";
import BoardList from "@/components/board-list";
import Link from "next/link";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { Article } from "../../../types";
import { useIsMo, useIsTa } from "@/hooks/useMediaQuery";
import { useOutsideClick } from "@/hooks/useOutsideClick";
export default function Page() {
+ const isMo = useIsMo();
+ const isTa = useIsTa();
const [sortState, setSortState] = useState(false);
const [order, setOrder] = useState("recent");
const [keyword, setKeyword] = useState("");
@@ -16,18 +18,10 @@ export default function Page() {
const [list, setList] = useState([]);
const [commonList, setCommonList] = useState([]);
const [isLoading, setIsLoading] = useState(true);
- const [pageSize, setPageSize] = useState(10);
- const [bestPageSize, setBestPageSize] = useState(3);
-
- const isMo = useIsMo();
- const isTa = useIsTa();
-
- const [isMount, setIsMount] = useState(false);
-
- useEffect(() => {
- setIsMount(true);
- }, []);
-
+ const [pageSize, setPageSize] = useState(() => (isMo ? 5 : isTa ? 7 : 10));
+ const [bestPageSize, setBestPageSize] = useState(() =>
+ isMo ? 1 : isTa ? 2 : 3
+ );
useEffect(() => {
if (isMo) {
setPageSize(5);
@@ -39,16 +33,31 @@ export default function Page() {
setPageSize(10);
setBestPageSize(3);
}
- }, [isMo, isTa, isMount]);
+ }, [isMo, isTa]);
+
const onSortToggle = () => {
setSortState(!sortState);
};
- const fetchData = async () => {
+ const fetchData = useCallback(async () => {
try {
- const bestResponse = await fetchBoardList(1, bestPageSize, "like");
- const commonResponse = await fetchBoardList(1, pageSize, order, keyword);
+ /**
+ * 아래코드의 문제점
+ * 두개를 각각 처리. 첫 번째 요청이 완료된 후 두번째 요청을 시작하게되므로 느림.
+ * 이러한걸 waterfall(순차처리)된다고 함. 주로 비동기 작업에 활용됨.
+ *
+ * const bestResponse = await fetchBoardList(1, bestPageSize, "like");
+ * const commonResponse = await fetchBoardList(1, pageSize, order, keyword);
+ *
+ * 해결 방법
+ * Promise.all을 활용해서 병렬로 처리해서 시간을 단축시킴.
+ * 구조분해문법 사용해서 좀 더 깔끔하게 처리
+ */
+ const [bestResponse, commonResponse] = await Promise.all([
+ fetchBoardList(1, bestPageSize, "like"),
+ fetchBoardList(1, pageSize, order, keyword),
+ ]);
setList(bestResponse);
setCommonList(commonResponse);
@@ -57,11 +66,15 @@ export default function Page() {
} finally {
setIsLoading(false);
}
- };
+ }, [order, keyword, pageSize, bestPageSize]);
+ /**
+ * useEffect 의존성 배열에 fetchData 추가
+ * useCallback 사용으로 불필요한 랜더링과 함수의 재성성 방지
+ */
useEffect(() => {
fetchData();
- }, [order, keyword, pageSize, bestPageSize]);
+ }, [fetchData]);
const sortChange = (state: string) => {
if (state !== order) {
@@ -74,30 +87,19 @@ export default function Page() {
setSearch(e.target.value);
};
- const onSearchEnter: React.KeyboardEventHandler = (e) => {
- if (e.key === "Enter") {
- setKeyword(search);
- setTimeout(() => {
- setSearch("");
- }, 100);
- }
+ const onSearchSubmit: React.FormEventHandler = (e) => {
+ e.preventDefault();
+ setKeyword(search);
+ setTimeout(() => {
+ setSearch("");
+ }, 100);
+
};
const ref = useOutsideClick(() => {
setSortState(false);
});
- /**
- * 질문하기 [Next.js + useMediaQuery]
- * 모바일, 태블릿 사이즈에서 새로고침 시 처음에 피씨가 잠깐 적용됨
- * isMount를 적용해도 안됨..
- */
- isMo && isMount
- ? console.log("모바일")
- : isTa && isMount
- ? console.log("타블렛")
- : console.log("피씨");
-
const handleScroll = () => {
const bottom =
window.innerHeight + document.documentElement.scrollTop ===
@@ -129,31 +131,30 @@ export default function Page() {
-
+
- {isMo && isMount ? (
+
- ) : order === "recent" ? (
- "최신순"
- ) : (
- "좋아요순"
- )}
+
+
+ {order === "recent" ? "최신순" : "좋아요순"}
+
{sortState && (
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 56f56da24..bf2d943e4 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -128,6 +128,7 @@ body {
}
.page_wrap {
margin-top: 24px;
+ padding-bottom: 130px;
}
.inner {
max-width: 1200px;
@@ -161,6 +162,21 @@ body {
font-size: 16px;
font-weight: 600;
}
+.btn.round {
+ border-radius: 999px;
+}
+.btn.img {
+ gap: 8px;
+}
+.btn.img > img {
+ height: 24px;
+ font-size: 0;
+}
+.btn:disabled {
+ background-color: #9ca3af;
+ color: #fff;
+ cursor: default;
+}
.empty_box {
width: 100%;
padding: 100px 0;
diff --git a/types.ts b/types.ts
index 78ec06ab9..9d08917dc 100644
--- a/types.ts
+++ b/types.ts
@@ -18,3 +18,25 @@ export interface BoardListResponse {
list: Article[];
totalCount: number;
}
+export interface Review extends Article {
+ isLiked: boolean;
+}
+
+export interface CommentWriter {
+ id: number;
+ nickname: string;
+ image: string | null;
+}
+
+export interface Comment {
+ id: number;
+ content: string;
+ createdAt: string;
+ updatedAt: string;
+ writer: CommentWriter;
+}
+
+export interface CommentListResponse {
+ list: Comment[];
+ nextCursor: string | null;
+}
\ No newline at end of file