Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[김세환] sprint10 #304

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"cSpell.words": ["sortmobile"]
"cSpell.words": ["addboard", "addfile", "sortmobile"]
}
27 changes: 27 additions & 0 deletions apis/addBoard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import axios from "@/lib/axios";

interface Params<T> {
data: T;
token: string;
}

export const postNewArticle = async ({ data, token }: Params<object>) => {
const res = await axios.post("/articles", data, {
headers: { Authorization: `Bearer ${token}` },
});
};

export const postUploadImage = async ({ data, token }: Params<File>) => {
const formData = new FormData();
formData.append("image", data);
try {
const res = await axios.post("/images/upload", formData, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return res.data;
} catch (err) {
console.log(err);
}
};
29 changes: 27 additions & 2 deletions apis/article.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import axios from "@/lib/axios";
import { ArticleQuery } from "@/models/article";
import { ArticleResponse } from "@/models/article";
import {
Article,
ArticleQuery,
CommentResponse,
GetCommentParam,
} from "@/types/article";
import { ArticleResponse } from "@/types/article";

export const getArticleList = async ({
page = 1,
Expand All @@ -16,3 +21,23 @@ export const getArticleList = async ({
const { list, totalCount }: ArticleResponse = res.data;
return { list, totalCount };
};

export const getArticlesById = async (articleId: number) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p4;
단일 아티클을 fetching하는 것이므로 getArticleById와 같이 단수형을 사용해도 좋을 것 같아요!

const res = await axios.get(`/articles/${articleId}`);
const { data }: { data: Article } = res;
return data;
};

export const getArticleComment = async ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p4;
요경우에는 commentList 혹은 comments가 되는게 맞는 것 같습니다!

articleId,
limit = 5,
cursor,
}: GetCommentParam) => {
const res = await axios.get(
`/articles/${articleId}/comments?limit=${limit}${
cursor ? `&cursor=${cursor}` : ""
}`
);
const { data }: { data: CommentResponse } = res;
return data;
};
24 changes: 24 additions & 0 deletions apis/comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import axios from "@/lib/axios";

interface PostArticleCommentParams {
articleId: number;
data: string;
token: string;
}

export const postArticleComment = async ({
articleId,
data,
token,
}: PostArticleCommentParams) => {
try {
const body = {
content: data,
};
const res = await axios.post(`/articles/${articleId}/comments`, body, {
headers: { Authorization: `Bearer ${token}` },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3;
토큰을 로컬스토리지로 관리하게 되면 이렇게 매번 headers에 token value를 전달하여 사용하지 않고,
axios의 interceptor를 사용하여 요청을 서버로 보내기 전에 그 요청을 가로채어 header 설정을 할 수 있을 것 같아요.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ex)

const instance = axios.create({
  baseURL: 'https://panda-market-api.vercel.app',
});

instance.interceptors.request.use((config) => {
  const token = localStorage.getItem('accessToken'); 

  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

export default instance;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그리고 jwt 토큰에는 일반적으로 만료기한이 설정되어 있습니다.
서버에서 전달 받은 jwt를 디코딩하면 만료 기한(Exp)을 확인하실 수 있습니다.

따라서, 로컬스토리지에 저장되어 있는 accessToken의 값을 디코딩하여,
만료 시간을 파악하고 토큰이 만료된 경우, 새로운 accessToken을 발급받는 등의 처리도 하실 수 있습니다

예를들어 Jwt의 유효 시간을 확인하는 방식은 아래와 같습니다.

// jwt 디코딩
const decodeToken = (token) => {
  const payload = token.split('.')[1];
  const decodedPayload = atob(payload);
  return JSON.parse(decodedPayload);
}

// 토큰의 유효 시간 확인 함수
const isTokenExpired = (token) => {
  const decoded = decodeToken(token);
  const currentTime = Math.floor(Date.now() / 1000);

  return decoded.exp < currentTime; // exp가 현재 시간보다 이전이면 토큰 만료
}

});
} catch (err) {
console.log(err);
}
};
18 changes: 18 additions & 0 deletions apis/signIn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import axios from "@/lib/axios";
import { access } from "fs";

interface AuthData {
email: string;
password: string;
}

interface ResponseData {
accessToken: string;
refreshToken: string;
}

export const postSignIn = async (authData: AuthData) => {
const res = await axios.post("/auth/signIn", authData);
const { accessToken, refreshToken }: ResponseData = res.data;
return { accessToken, refreshToken };
};
51 changes: 51 additions & 0 deletions components/addboard/AddBoardForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Button from "@/components/ui/Button";
import FileInput from "@/components/ui/FileInput";
import Input from "@/components/ui/Input";
import TextArea from "@/components/ui/TextArea";
import useAddBoard from "@/hooks/useAddBoard";

export default function AddBoardForm() {
const {
inputValues,
onChangeInput,
onSubmitForm,
onChangeFileInput,
isInputValid,
} = useAddBoard();
return (
<form className="m-container mt-[70px] pt-4">
<div className="flex justify-between">
<span className="font-bold text-xl">게시글 쓰기</span>
<Button
className="btn w-[74px] h-[42px]"
onClick={onSubmitForm}
activeBtn={isInputValid}
>
등록
</Button>
</div>
<Input
name="title"
placeholder="제목을 입력해주세요"
type="input"
label="*제목"
value={inputValues.title}
onChange={onChangeInput}
/>
<TextArea
name="content"
placeholder="내용을 입력해 주세요"
label="*내용"
value={inputValues.content}
onChange={onChangeInput}
/>
<FileInput
value=""
onChange={onChangeFileInput}
name="image"
label="이미지"
image={inputValues.image}
/>
</form>
);
}
28 changes: 17 additions & 11 deletions components/boards/Article.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Image from "next/image";
import { formatDate } from "@/utils/formatDate";
import type { Article } from "@/models/article";
import type { Article } from "@/types/article";

import icHeart from "@/public/images/ic_heart.svg";
import icProfile from "@/public/images/ic_profile.svg";
import { Dispatch, SetStateAction } from "react";
import Link from "next/link";
interface Props {
articles: Article[];
setTarget: Dispatch<SetStateAction<HTMLLIElement | null>>;
Expand All @@ -18,18 +19,23 @@ export default function Article({ articles, setTarget }: Props) {
<li
ref={index === articles.length - 1 ? setTarget : null}
key={`${article.id}+${index}`}
className=" bg-gray-50 px-2 pb-6 pt-1 mt-5 border-b shadow-md rounded-xl border-gray-200"
className=" bg-gray-50 px-2 pb-6 pt-2 mt-5 border-b shadow-md rounded-xl border-gray-200 h-[136px]"
>
<div className="flex h-full flex-col justify-between">
<Link
href={`/boards/${article.id}`}
className="flex h-full flex-col justify-between"
>
<div className="flex justify-between gap-10">
<div className="font-semibold text-lg">{article.content}</div>
<Image
className="rounded-lg border border-gray-200 max-h-[72px]"
width={72}
height={72}
src={article.image}
alt="게시글 이미지"
/>
{article.image && (
<Image
className="rounded-lg border border-gray-200 max-h-[72px]"
width={72}
height={72}
src={article.image}
alt="게시글 이미지"
/>
)}
</div>
<div className="text-sm gap-2 flex justify-between pt-4">
<Image
Expand All @@ -53,7 +59,7 @@ export default function Article({ articles, setTarget }: Props) {
{article.likeCount}
</span>
</div>
</div>
</Link>
</li>
))}
</ul>
Expand Down
24 changes: 17 additions & 7 deletions components/boards/ArticleList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import icGlasses from "@/public/images/ic_glasses.svg";
import icSort from "@/public/images/ic_sort.svg";
import icSortMobile from "@/public/images/ic_sortmobile.svg";
import DropDown from "@/components/ui/DropDown";
import Link from "next/link";

const INIT_ORDERS = {
recent: "최신순",
Expand All @@ -21,7 +22,7 @@ export default function ArticleList() {
const [nowOrderBy, setNowOrderBy] = useState<OrderByType>(INIT_ORDERS.recent);
const [isOpenDropDown, setIsOpenDropDown] = useState(false);

const onClickDropDown = () => {
const toggleDropDown = () => {
setIsOpenDropDown((prev) => !prev);
};

Expand All @@ -32,7 +33,7 @@ export default function ArticleList() {
orderBy: orderBy,
}));
setNowOrderBy(INIT_ORDERS[orderBy]);
setIsOpenDropDown((prev) => !prev);
toggleDropDown();
};

let debounceTimer: NodeJS.Timeout;
Expand All @@ -54,13 +55,22 @@ export default function ArticleList() {
<div className="pt-10">
<div className="flex justify-between ">
<span className="font-bold text-lg">게시글</span>
<button className="btn order-1 h-[42px] w-[88px] font-semibold">
<Link
href="/addboard"
className="btn order-1 h-[42px] w-[88px] font-semibold"
>
글쓰기
</button>
</Link>
</div>
<div className="flex relative pt-6 gap-4">
<div className=" flex basis-72 gap-1.5 rounded-xl bg-slate-100 px-4 py-[9px] grow">
<Image src={icGlasses} width={15} height={15} alt="게시글 검색" />
<Image
src={icGlasses}
className="w-[15px] h-[15px]"
width={15}
height={15}
alt="게시글 검색"
/>
<input
onChange={onChangeKeyword}
placeholder="검색할 상품을 입력해주세요"
Expand All @@ -69,14 +79,14 @@ export default function ArticleList() {
</div>

<button
onClick={onClickDropDown}
onClick={toggleDropDown}
className="flex-center h-[42px] w-[42px] rounded-xl border border-solid border-gray-200 md:hidden"
>
<Image src={icSortMobile} alt="정렬하기" />
</button>

<button
onClick={onClickDropDown}
onClick={toggleDropDown}
className="hidden h-[42px] w-[130px] items-center justify-between rounded-xl border border-solid px-5 py-4 md:flex"
>
<span>{nowOrderBy}</span>
Expand Down
71 changes: 39 additions & 32 deletions components/boards/BestArticle.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Image from "next/image";

import { formatDate } from "@/utils/formatDate";
import { Article } from "@/models/article";
import { Article } from "@/types/article";

import icMedal from "@/public/images/ic_medal.svg";
import icHeart from "@/public/images/ic_heart.svg";
import Link from "next/link";
interface Props {
articles: Article[];
}
Expand All @@ -18,38 +19,44 @@ export default function BestArticle({ articles }: Props) {
key={article.id}
className="grow bg-gray-50 shadow-md h-[198px] lg:h-[169px] px-[22px] pt-[46px] pb-4 relative rounded-lg"
>
<span className="absolute gap-1 flex-center top-0 left-[22px] w-[102px] h-[30px] text-white bg-my-blue rounded-b-2xl">
<Image src={icMedal} alt="베스트 게시글 뱃지" />
Best
</span>
<div className="flex h-full flex-col justify-between">
<div className="flex justify-between gap-10">
<div className="font-semibold text-lg">{article.content}</div>
<Image
className="rounded-lg border border-gray-200 max-h-[72px]"
width={72}
height={72}
src={article.image}
alt="게시글 이미지"
/>
<Link href={`/boards/${article.id}`}>
<span className="absolute gap-1 flex-center top-0 left-[22px] w-[102px] h-[30px] text-white bg-my-blue rounded-b-2xl">
<Image src={icMedal} alt="베스트 게시글 뱃지" />
Best
</span>
<div className="flex h-full flex-col justify-between">
<div className="flex justify-between gap-10">
<div className="font-semibold text-lg">{article.content}</div>
{article.image && (
<Image
className="rounded-lg border border-gray-200 max-h-[72px]"
width={72}
height={72}
src={article.image}
alt="게시글 이미지"
/>
)}
</div>
<div className="text-sm gap-2 flex justify-between">
<span className="text-gray-700">
{article.writer.nickname}
</span>
<span className="flex gap-0.5 grow text-gray-500">
<Image
src={icHeart}
className="mt-[2px]"
width={13.4}
height={11.65}
alt="좋아요"
/>
{article.likeCount}
</span>
<span className="text-gray-400">
{formatDate(article.createdAt)}
</span>
</div>
</div>
<div className="text-sm gap-2 flex justify-between">
<span className="text-gray-700">{article.writer.nickname}</span>
<span className="flex gap-0.5 grow text-gray-500">
<Image
src={icHeart}
className="mt-[2px]"
width={13.4}
height={11.65}
alt="좋아요"
/>
{article.likeCount}
</span>
<span className="text-gray-400">
{formatDate(article.createdAt)}
</span>
</div>
</div>
</Link>
</li>
))}
</ul>
Expand Down
Loading
Loading