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} +
    +
    +

    {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 ( + <> +
    +
    게시글 쓰기
    + +
    +
    +
      +
    • +
      제목
      +
      + +
      +
    • +
    • +
      내용
      +
      + +
      +
    • +
    • +
      이미지
      +
      + + {!imageSrc && ( + + )} + {imageSrc && ( +
      +
      + 이미지삭제 +
      + {imageSrc} +
      + )} +
      +
    • +
    +
    + + ); +} 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} +
    +

    {data.writer.nickname}

    + {formatDate(data.updatedAt)} +
    +
    + {data.isLiked ? ( + 좋아요 버튼 + ) : ( + 좋아요 버튼 + )} + {data.likeCount} +
    +
    +
    + +
    + {data.image ? {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