diff --git a/public/images/ic_back_arrow.svg b/public/images/ic_back_arrow.svg new file mode 100644 index 000000000..253a47d7b --- /dev/null +++ b/public/images/ic_back_arrow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/ic_kebab_btn.png b/public/images/ic_kebab_btn.png new file mode 100644 index 000000000..a30edcdc7 Binary files /dev/null and b/public/images/ic_kebab_btn.png differ diff --git a/public/images/ic_register_img_file.png b/public/images/ic_register_img_file.png new file mode 100644 index 000000000..0152f01cd Binary files /dev/null and b/public/images/ic_register_img_file.png differ diff --git a/public/images/img_del_btn_default.png b/public/images/img_del_btn_default.png new file mode 100644 index 000000000..aa81e538c Binary files /dev/null and b/public/images/img_del_btn_default.png differ diff --git a/public/images/img_no_comments.png b/public/images/img_no_comments.png new file mode 100644 index 000000000..75e54780d Binary files /dev/null and b/public/images/img_no_comments.png differ diff --git a/src/apis/getArticles.ts b/src/apis/getArticles.ts deleted file mode 100644 index a6a676351..000000000 --- a/src/apis/getArticles.ts +++ /dev/null @@ -1,48 +0,0 @@ -import instance from "./instance"; - -interface Writer { - id: number; - nickname: string; -} - -export interface Article { - id: number; - title: string; - content: string; - image: string; - likeCount: number; - writer: Writer; - createdAt: string; - updatedAt: string; -} - -export type orderOption = "like" | "recent"; - -interface GetArticlesProps { - page?: number; - pageSize: number; - orderBy?: orderOption; - keyword?: string; -} - -interface Response { - list: Article[]; - totalCount: number; -} - -export default async function getArticles({ - page = 1, - pageSize, - orderBy = "recent", - keyword = "", -}: GetArticlesProps) { - let query = `page=${page}&pageSize=${pageSize}&orderBy=${orderBy}`; - if (keyword) { - query += `&keyword=${keyword}`; - } - const res = await instance.get(`/articles?${query}`); - const { list, totalCount }: Response = res.data; - - // 예외 처리 및 에러 처리 필요 - return { list, totalCount }; -} diff --git a/src/apis/instance.ts b/src/apis/instance.ts deleted file mode 100644 index 8200de696..000000000 --- a/src/apis/instance.ts +++ /dev/null @@ -1,7 +0,0 @@ -import axios from "axios"; - -const instance = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_URL, -}); - -export default instance; diff --git a/src/axios/articles.ts b/src/axios/articles.ts new file mode 100644 index 000000000..9e7844fa8 --- /dev/null +++ b/src/axios/articles.ts @@ -0,0 +1,86 @@ +import axiosInstance from "./instance"; + +interface Writer { + id: number; + nickname: string; +} + +export interface Article { + id: number; + title: string; + content: string; + image: string | null; + likeCount: number; + writer: Writer; + createdAt: string; + updatedAt: string; +} + +export type OrderOption = "like" | "recent"; + +interface GetArticlesProps { + page?: number; + pageSize: number; + orderBy?: OrderOption; + keyword?: string; +} + +interface ArticlesResponse { + list: Article[]; + totalCount: number; +} + +export async function getArticles({ + page = 1, + pageSize, + orderBy = "recent", + keyword = "", +}: GetArticlesProps) { + let query = `page=${page}&pageSize=${pageSize}&orderBy=${orderBy}`; + if (keyword) { + query += `&keyword=${keyword}`; + } + + try { + const res = await axiosInstance.get(`/articles?${query}`); + const { list, totalCount }: ArticlesResponse = res.data; + return { list, totalCount }; + } catch (error) { + return { list: [], totalCount: 0 }; + } +} + +interface GetArticleByIDProps { + articleId: number; +} + +export async function getArticleByID({ articleId }: GetArticleByIDProps) { + const res = await axiosInstance.get(`/articles/${articleId}`); + try { + const article: Article = res.data; + return article; + } catch { + throw new Error("게시글 응답이 올바르지 않습니다."); + } +} + +export interface PostArticleProps { + image: string | null; + content: string; + title: string; +} + +export async function postArticle({ image, content, title }: PostArticleProps) { + try { + let res; + if (image) { + res = await axiosInstance.post("/articles", { image, content, title }); + } else { + res = await axiosInstance.post("/articles", { content, title }); + } + const postedArticle: Article = res.data; + return postedArticle; + } catch (error) { + console.log(error); + } +} diff --git a/src/axios/auth.ts b/src/axios/auth.ts new file mode 100644 index 000000000..01b29928f --- /dev/null +++ b/src/axios/auth.ts @@ -0,0 +1,47 @@ +import axiosInstance from "./instance"; + +export interface LogInUserProps { + email: string; + password: string; +} + +export interface SignUpUserProps extends LogInUserProps { + nickname: string; + passwordConfirmation: string; +} + +export async function signUpUser({ + email, + nickname, + password, + passwordConfirmation, +}: SignUpUserProps) { + try { + const res = await axiosInstance.post("/auth/signUp", { + email, + nickname, + password, + passwordConfirmation, + }); + const { accessToken, refreshToken } = res.data; + + localStorage.setItem("access_token", accessToken); + localStorage.setItem("refresh_token", refreshToken); + window.location.href = "/"; + } catch (error) { + console.log(error); + } +} + +export async function logInUser({ email, password }: LogInUserProps) { + try { + const res = await axiosInstance.post("/auth/signIn", { email, password }); + const { accessToken, refreshToken } = res.data; + + localStorage.setItem("access_token", accessToken); + localStorage.setItem("refresh_token", refreshToken); + window.location.href = "/"; + } catch (error) { + console.log(error); + } +} diff --git a/src/axios/comments.ts b/src/axios/comments.ts new file mode 100644 index 000000000..cab410426 --- /dev/null +++ b/src/axios/comments.ts @@ -0,0 +1,46 @@ +import axiosInstance from "./instance"; + +interface Writer { + id: number; + image: string; + nickname: string; +} + +export interface Comment { + id: number; + content: string; + updatedAt: string; + createdAt: string; + writer: Writer; +} + +interface GetArticleCommentsProps { + articleId: number; + limit: number; + cursor?: number; +} + +export interface ArticleCommentsResponse { + nextCursor: number | null; + list: Comment[]; +} + +export async function getArticleComments({ articleId, limit, cursor }: GetArticleCommentsProps) { + let query = `limit=${limit}`; + if (cursor) { + query += `&cursor=${cursor}`; + } + const res = await axiosInstance.get(`/articles/${articleId}/comments?${query}`); + const { nextCursor, list }: ArticleCommentsResponse = res.data; + return { nextCursor, list }; +} + +interface PostArticleCommentProps { + articleId: number; + content: string; +} + +export async function postArticleComment({ articleId, content }: PostArticleCommentProps) { + const res = await axiosInstance.post(`/articles/${articleId}/comments`, { content }); + return res.data as Comment; +} diff --git a/src/axios/images.ts b/src/axios/images.ts new file mode 100644 index 000000000..f80817dd7 --- /dev/null +++ b/src/axios/images.ts @@ -0,0 +1,21 @@ +import axiosInstance from "./instance"; + +interface UploadImageProps { + imageFile: File; +} + +export default async function uploadImage({ imageFile }: UploadImageProps) { + // image file을 form data로 변경 + const formDataForSubmit = new FormData(); + formDataForSubmit.append("image", imageFile); + + try { + const res = await axiosInstance.post("/images/upload", formDataForSubmit, { + headers: { "Content-Type": "multipart/form-data" }, + }); + const { url } = res.data; + return url; + } catch (error) { + console.log(error); + } +} diff --git a/src/axios/instance.ts b/src/axios/instance.ts new file mode 100644 index 000000000..12376a287 --- /dev/null +++ b/src/axios/instance.ts @@ -0,0 +1,74 @@ +import axios, { AxiosResponse, AxiosError, InternalAxiosRequestConfig } from "axios"; + +const axiosInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, +}); + +/** + * http request가 넘어가기 전에 call 되는 함수 + */ +const onRequest = (config: InternalAxiosRequestConfig) => { + if (typeof window !== "undefined") { + const accessToken = localStorage.getItem("access_token"); + + if (accessToken && config.headers) { + config.headers["Authorization"] = `Bearer ${accessToken}`; + } + } + return config; +}; + +/** + * http response가 catch로 넘어가기 전에 call 되는 함수 + */ +const onErrorResponse = async (error: AxiosError | Error) => { + if (axios.isAxiosError(error)) { + const { status, data, config } = error.response as AxiosResponse; + + switch (status) { + case 400: { + alert("입력한 정보가 올바르지 않습니다."); + break; + } + + case 401: { + const refreshToken = localStorage.getItem("refresh_token"); + + if (refreshToken) { + if (data.message === "jwt expired") { + const res = await axiosInstance.post("/auth/refresh-token", { refreshToken }); + const { accessToken } = res.data; + + localStorage.setItem("access_token", accessToken); + + config.headers["Authorization"] = `Bearer ${accessToken}`; + return axiosInstance(config); + } else if (data.message === "jwt malformed") { + localStorage.removeItem("access_token"); + localStorage.removeItem("refresh_token"); + alert("세션이 올바르지 않습니다. 다시 로그인 해주세요."); + window.location.href = "/login"; + } else { + console.log(data.message); + } + } else { + alert("로그인이 필요합니다."); + window.location.href = "/login"; + } + break; + } + + default: { + console.log(error.message); + break; + } + } + } + + return Promise.reject(error); +}; + +axiosInstance.interceptors.request.use(onRequest); +axiosInstance.interceptors.response.use((response: AxiosResponse) => response, onErrorResponse); + +export default axiosInstance; diff --git a/src/components/@shared/BlueButton.tsx b/src/components/@shared/BlueButton.tsx index 0044b5755..ad07affd2 100644 --- a/src/components/@shared/BlueButton.tsx +++ b/src/components/@shared/BlueButton.tsx @@ -11,9 +11,10 @@ interface ButtonProps extends React.ButtonHTMLAttributes { * @param shape default(0.5rem), pill(알약형) 중 한가지 선택 * @returns 공통 스타일 버튼 컴포넌트 */ -export default function BlueButton({ customStyle, shape, children }: ButtonProps) { +export default function BlueButton({ customStyle, shape, children, ...rest }: ButtonProps) { return ( + + )} + + + + ); +} diff --git a/src/components/@shared/inputs/GrayInput.tsx b/src/components/@shared/inputs/GrayInput.tsx new file mode 100644 index 000000000..770af3fd3 --- /dev/null +++ b/src/components/@shared/inputs/GrayInput.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +interface GrayInputProps extends React.InputHTMLAttributes { + customStyle?: string; +} + +export default function GrayInput({ customStyle, children, ...props }: GrayInputProps) { + return ( + + {children} + + ); +} diff --git a/src/components/@shared/inputs/GrayTextarea.tsx b/src/components/@shared/inputs/GrayTextarea.tsx new file mode 100644 index 000000000..989f7918c --- /dev/null +++ b/src/components/@shared/inputs/GrayTextarea.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +interface GrayInputProps extends React.TextareaHTMLAttributes { + customStyle?: string; +} + +export default function GrayTextarea({ customStyle, children, ...props }: GrayInputProps) { + return ( + + ); +} diff --git a/src/components/boards/BasicPostCard.tsx b/src/components/boards/BasicPostCard.tsx index 8ff63f346..05f161d35 100644 --- a/src/components/boards/BasicPostCard.tsx +++ b/src/components/boards/BasicPostCard.tsx @@ -1,39 +1,54 @@ -import { Article } from "@/apis/getArticles"; +import { Article } from "@/axios/articles"; import createDateStringWithDot from "@/utils/createDateStringWithDot"; import Image from "next/image"; +import { useRouter } from "next/router"; +import Link from "next/link"; interface BasicPostCardProps { article: Article; } export default function BasicPostCard({ article }: BasicPostCardProps) { + const { pathname } = useRouter(); return ( -
-
-

{article.title}

- 게시글 이미지 -
-
-
- -
{article.writer.nickname}
-
- {createDateStringWithDot(article.createdAt)} + +
+
+

+ {article.title} +

+
+ 게시글 이미지
-
- -
{article.likeCount}
+
+
+ 작성자 프로필 +
{article.writer.nickname}
+
+ {createDateStringWithDot(article.createdAt)} +
+
+
+ +
+ {article.likeCount < 1000 ? article.likeCount : "999+"} +
+
-
-
+
+ ); } diff --git a/src/components/boards/BestPostCard.tsx b/src/components/boards/BestPostCard.tsx index 2d99a7f7b..ac73edbba 100644 --- a/src/components/boards/BestPostCard.tsx +++ b/src/components/boards/BestPostCard.tsx @@ -1,52 +1,60 @@ -import { Article } from "@/apis/getArticles"; +import { Article } from "@/axios/articles"; import createDateStringWithDot from "@/utils/createDateStringWithDot"; import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; interface BestPostCardProps { article: Article; } export default function BestPostCard({ article }: BestPostCardProps) { + const { pathname } = useRouter(); return ( -
- 베스트 게시글 뱃지 -
-
-

{article.title}

- 게시글 이미지 -
-
-
-
{article.writer.nickname}
-
- -
{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 ( +
  • +
    +
    {content}
    + +
    +
    +
    + 댓글 작성자 프로필 +
    +
    +
    {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)} +
    +
    +
    + +
    {likeCount}
    +
    +
    +
    + {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", }, }, },