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

[김보미] sprint11 #670

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
016f847
📦 chore: env 파일 gitignore 추가
Kbomi16 Jun 12, 2024
bbb8b6d
📦 chore: env파일 git에서 제거
Kbomi16 Jun 12, 2024
aa7c6a7
♻️ refactor: axios URLSearchParams 추가
Kbomi16 Jun 12, 2024
c4a9dfc
🔥 remove: 장식 이미지 빈 alt 로 설정
Kbomi16 Jun 12, 2024
358bb04
♻️ refactor: 테일윈드 공통 변수 재적용
Kbomi16 Jun 12, 2024
6198fb2
📝 docs: 미션11 README 업데이트
Kbomi16 Jun 12, 2024
399dc64
♻️ refactor: 페이지네이션 useMemo
Kbomi16 Jun 12, 2024
76c942f
♻️ refactor: post api 분리
Kbomi16 Jun 12, 2024
37ac08b
📦 chore: react-hook-form 설치
Kbomi16 Jun 12, 2024
bcb1f56
♻️ refactor: react-hook-form 으로 변경
Kbomi16 Jun 12, 2024
0964c90
✨ feat: 회원가입 시 로그인 페이지로 이동
Kbomi16 Jun 12, 2024
02ebb7a
♻️ refactor: 새로고침 안 하고 바로 반영하기
Kbomi16 Jun 12, 2024
429dbd0
♻️ refactor: 댓글 등록시 바로 반영
Kbomi16 Jun 12, 2024
a18ba75
📝 docs: 심화 요구사항 업데이트
Kbomi16 Jun 13, 2024
d1022fe
♻️ refactor: axios interceptor 추가
Kbomi16 Jun 13, 2024
8ad9474
♻️ refactor: 로그인시 refresh 토큰 받기
Kbomi16 Jun 13, 2024
ffe2c50
♻️ refactor: get instance 분리
Kbomi16 Jun 13, 2024
a695419
♻️ refactor: get instance 분리
Kbomi16 Jun 13, 2024
90f8377
♻️ refactor: 로그아웃시 refreshToken 제거
Kbomi16 Jun 13, 2024
c8a1e4b
✨ feat: 로그아웃시 메인으로 이동
Kbomi16 Jun 13, 2024
aa189e9
♻️ refactor: axios 인터셉터 설정
Kbomi16 Jun 13, 2024
e42f9d7
🐛 fix: axios token 갱신
Kbomi16 Jun 13, 2024
690b18c
💄 ui: 목록 버튼 호버 적용
Kbomi16 Jun 14, 2024
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
1 change: 0 additions & 1 deletion .env

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel
Expand Down
30 changes: 17 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@

- Github에 PR(Pull Request)을 만들어서 미션을 제출합니다.
- 피그마 디자인에 맞게 페이지를 만들어 주세요.
- 기존의 React, Typescript로 구현한 프로젝트와 별도로 진행합니다.
- Next.js를 사용합니다
- 기존의 스프린트 미션 8에 이어서 React, Typescript를 사용합니다.

체크리스트 [기본] <br>

상품 등록 페이지
회원가입

- 상품 등록 페이지 주소는 “/addboard” 입니다.
- 게시판 이미지는 최대 한개 업로드가 가능합니다.
- 각 input의 placeholder 값을 정확히 입력해주세요.
- 이미지를 제외하고 input 에 모든 값을 입력하면 ‘등록' 버튼이 활성화 됩니다.
- 회원가입, 로그인 api를 사용하여 받은accessToken을 사용하여 게시물 등록을 합니다.
- ‘등록’ 버튼을 누르면 게시물 상세 페이지로 이동합니다.
- 유효한 정보를 입력하고 스웨거 명세된 “/auth/signUp”으로 POST 요청해서 성공 응답을 받으면 회원가입이 완료됩니다.
- 회원가입이 완료되면 “/login”로 이동합니다.
- 회원가입 페이지에 접근시 로컬 스토리지에 accessToken이 있는 경우 ‘/’ 페이지로 이동합니다.

상품 상세 페이지
로그인

- 상품 상세 페이지 주소는 “/addboard/{id}” 입니다.
- 댓글 input 값을 입력하면 ‘등록' 버튼이 활성화 됩니다.
- 활성화된 ‘등록' 버튼을 누르면 댓글이 등록됩니다
- 회원가입을 성공한 정보를 입력하고 스웨거 명세된 “/auth/signIn”으로 POST 요청을 하면 로그인이 완료됩니다.
- 로그인이 완료되면 로컬 스토리지에 accessToken을 저장하고 “/” 로 이동합니다.
- 로그인/회원가입 페이지에 접근시 로컬 스토리지에 accessToken이 있는 경우 ‘/’ 페이지로 이동합니다.

메인

- 로컬 스토리지에 accessToken이 있는 경우 상단바 ‘로그인’ 버튼이 판다 이미지로 바뀝니다.

심화 요구사항

- 로그인, 회원가입 기능에 react-hook-form을 활용해봅니다.
89 changes: 80 additions & 9 deletions api/api.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import instance from "@/lib/axios";
import axiosGetInstance from "@/lib/axiosGetInstance";

// GET
// 페이지네이션을 위한 전체 게시글 수
export async function getTotalPosts() {
try {
const { data } = await instance.get(
"/articles?&pageSize=10000&orderBy=recent",
);
const params = new URLSearchParams({
pageSize: "10000",
orderBy: "recent",
});

const { data } = await axiosGetInstance.get("/articles?", { params });
Comment on lines +8 to +13
Copy link
Collaborator

Choose a reason for hiding this comment

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

굳굳 ! 훨신 깔끔해졌네요 😊

Suggested change
const params = new URLSearchParams({
pageSize: "10000",
orderBy: "recent",
});
const { data } = await axiosGetInstance.get("/articles?", { params });
const params = new URLSearchParams({
pageSize: "10000",
orderBy: "recent",
});
const { data } = await axiosGetInstance.get("/articles", { params });

위와 같이 ?는 지워도 될 것으로 보여요 !

return data.list.length;
} catch (error) {
console.error("getTotalPosts 함수에서 오류 발생:", error);
Expand All @@ -26,7 +31,9 @@ export async function getPosts({
page: page.toString(),
pageSize: pageSize.toString(),
});
const { data } = await instance.get(`/articles?${params.toString()}`);
const { data } = await axiosGetInstance.get(
`/articles?${params.toString()}`,
);
return data.list;
} catch (error) {
console.error("getPosts 함수에서 오류 발생:", error);
Expand All @@ -36,8 +43,13 @@ export async function getPosts({

export async function getBestPosts({ pageSize = 3 }) {
try {
const { data } = await instance.get(
`/articles?&pageSize=${pageSize}&orderBy=like`,
const params = new URLSearchParams({ orderBy: "like" });

const { data } = await axiosGetInstance.get(
`/articles?&pageSize=${pageSize}`,
{
params,
},
Comment on lines +48 to +52
Copy link
Collaborator

Choose a reason for hiding this comment

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

반환 타입도 지정해보면 어떨까요?

Suggested change
const { data } = await axiosGetInstance.get(
`/articles?&pageSize=${pageSize}`,
{
params,
},
const { data } = await axiosGetInstance.get<GetBestPosts>(
`/articles?&pageSize=${pageSize}`,
{
params,
},

);
return data.list;
} catch (error) {
Expand All @@ -48,7 +60,7 @@ export async function getBestPosts({ pageSize = 3 }) {

export async function getPostsDetail(articleId: string) {
try {
const { data } = await instance.get(`/articles/${articleId}`);
const { data } = await axiosGetInstance.get(`/articles/${articleId}`);
return data;
} catch (error) {
console.error("getPostsDetail 함수에서 오류 발생:", error);
Expand All @@ -58,12 +70,71 @@ export async function getPostsDetail(articleId: string) {

export async function getPostsComments(articleId: string) {
try {
const { data } = await instance.get(
`/articles/${articleId}/comments?limit=100`,
const params = new URLSearchParams({ limit: "100" });
const { data } = await axiosGetInstance.get(
`/articles/${articleId}/comments?`,
{
params,
},
);
return data.list;
} catch (error) {
console.error("getPostsComments 함수에서 오류 발생:", error);
throw error;
}
}

// POST
export async function postImages(formData: FormData, token: string) {
try {
const response = await instance.post("/images/upload", formData, {
headers: {
"Content-Type": "multipart/form-data",
Authorization: `Bearer ${token}`,
},
});
Comment on lines +91 to +95
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기만 Content-Type이 달라서 이렇게 처리하셨군요 👍👍

return response.data.url;
} catch (error) {
console.error("postImages 함수에서 오류 발생:", error);
}
}

type PostData = {
title: string;
content: string;
image?: string;
};

export async function postArticles(postData: PostData, token: string) {
try {
const response = await instance.post("/articles", postData, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
console.error("postArticles 함수에서 오류 발생:", error);
}
}

export async function postArticleComments(
articleId: string,
content: string,
token: string,
) {
try {
await instance.post(
`/articles/${articleId}/comments`,
{ content },
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
} catch (error) {
console.error("postArticleComments 함수에서 오류 발생:", error);
}
Comment on lines +137 to +139
Copy link
Collaborator

Choose a reason for hiding this comment

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

오잉? 이렇게 되면 함수 반환타입이 void이거나 any가 될 것으로 보입니다 !

Suggested change
} catch (error) {
console.error("postArticleComments 함수에서 오류 발생:", error);
}
} catch (error) {
console.error("postArticleComments 함수에서 오류 발생:", error);
throw error;
}

만약 에러가 발생하면 컴포넌트에서 캐치하여 처리해야하지 않을까요?

}
16 changes: 7 additions & 9 deletions components/BestPosts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,17 @@ export default function BestPosts({ initialBestPosts }: BestPostsProps) {
bestPosts.map((post) => (
<Link
href={`/boards/${post.id}`}
className="h-[169px] w-[400px] rounded-[1rem] bg-[var(--gray50)] px-8 md:w-[380px] xl:w-full"
className="h-[169px] w-[400px] rounded-[1rem] bg-gray-50 px-8 md:w-[380px] xl:w-full"
key={post.id}
>
<div className="inline-flex h-[30px] w-[102px] items-center justify-center rounded-b-[5rem] bg-[var(--main)]">
<Image src={icon_medal} alt="메달" width={16} height={16} />
<div className="bg-main inline-flex h-[30px] w-[102px] items-center justify-center rounded-b-[5rem]">
<Image src={icon_medal} alt="" width={16} height={16} />
<p className="text-[16px] text-white">Best</p>
</div>
<div className="flex h-[80px] items-center justify-between gap-[0.3rem]">
<h3 className="m-0 p-0 font-bold">{post.title}</h3>
{post.image && (
<div className="flex h-[72px] min-w-[72px] items-center justify-center rounded-[0.3rem] border-[1.5px] border-[var(--gray100)] bg-white">
<div className="flex h-[72px] min-w-[72px] items-center justify-center rounded-[0.3rem] border-[1.5px] border-gray-100 bg-white">
<Image
src={post.image}
alt="포스트 이미지"
Expand All @@ -126,15 +126,13 @@ export default function BestPosts({ initialBestPosts }: BestPostsProps) {
</div>
<div className="mt-4 flex items-end justify-between">
<div className="flex items-center justify-start gap-[0.2rem]">
<p className="text-[14px] text-[var(--gray600)]">
<p className="text-[14px] text-gray-600">
{post.writer.nickname}
</p>
<Image src={icon_favorite} alt="하트" width={16} height={16} />
<p className="text-[14px] text-[var(--gray600)]">
{post.likeCount}
</p>
<p className="text-[14px] text-gray-600">{post.likeCount}</p>
</div>
<p className="text-[14px] text-[var(--gray400)]">
<p className="text-[14px] text-gray-400">
{formatDate(post.createdAt)}
</p>
</div>
Expand Down
4 changes: 2 additions & 2 deletions components/FileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function FileInput({ name, value, initialPreview, onChange }: FileInputProps) {
}, [value, initialPreview]);

return (
<div className="rounded-0.5rem relative my-2 h-[162px] w-[162px] overflow-hidden rounded-md bg-[--coolgray100] lg:h-[282px] lg:w-[282px]">
<div className="bg-coolgray-100 relative my-2 h-[162px] w-[162px] overflow-hidden rounded-md lg:h-[282px] lg:w-[282px]">
<div className="flex h-full w-full items-center justify-center">
{preview ? (
<Image
Expand Down Expand Up @@ -80,7 +80,7 @@ function FileInput({ name, value, initialPreview, onChange }: FileInputProps) {
/>
{value && (
<button
className="absolute right-[9px] top-[9px] h-[26px] w-[26px] cursor-pointer rounded-full border-none bg-black bg-opacity-40 p-1 hover:bg-[--main]"
className="hover:bg-main absolute right-2.5 top-2.5 h-6 w-6 cursor-pointer rounded-full border-none bg-black bg-opacity-40 p-1"
onClick={handleClearClick}
>
<Image src={icon_reset} alt="선택해제" width={26} height={26} />
Expand Down
4 changes: 2 additions & 2 deletions components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import Image from "next/image";

function Footer() {
return (
<footer className="b-[0] relative mt-auto h-[160px] w-full bg-gray-800 py-6 text-white">
<footer className="b-[0] bg-footer relative mt-auto h-[160px] w-full py-6 text-white">
<div className="container mx-auto flex flex-col-reverse items-center justify-between md:flex-row">
<p className="relative -left-[10rem] top-[1rem] mb-4 text-sm md:left-0 md:top-0 md:mb-0 md:text-base">
©codeit - 2024
</p>
<div className="flex flex-row items-center md:justify-between md:gap-[20rem]">
<div className="relative -left-[5rem] top-2 mb-4 flex items-center space-x-4 text-white md:-left-0 md:top-0 md:mb-0">
<div className="text-footercodeit relative -left-[5rem] top-2 mb-4 flex items-center space-x-4 md:-left-0 md:top-0 md:mb-0">
<p className="text-sm md:text-base">
<Link href="./privacy.html">Privacy Policy</Link>
</p>
Expand Down
29 changes: 15 additions & 14 deletions components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,25 @@ export default function Header() {

useEffect(() => {
const token = localStorage.getItem("accessToken");
setAccessToken(token);
}, []);
if (token) {
setAccessToken(token);
}
}, [router.pathname]);
Comment on lines 22 to +27
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

전에 로그인 후 바로 적용이 안 되는 이슈가 있어서 reload()를 썼었는데 의존성 배열에 router.pathname을 넣으면 바로 반영되도록 수정했습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 되면 리다이렉트 시 변경을 감지하고 리렌더링 시켜주는 걸까요?

일단 논리적으로는 맞는 말이지만 근본적이진 못할 수 있을 것 같아요.
Context API를 통하여 헤더가 상태를 관리하고 있다면 어떨까요?


const handleProfileClick = () => {
setIsLogoutBoxVisible(!isLogoutBoxVisible);
};

const logout = () => {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
setAccessToken(null);
setIsLogoutBoxVisible(false);
router.push("/");
window.location.reload();
};

return (
<div className="fixed top-0 z-50 flex h-16 w-full items-center justify-between gap-4 bg-white px-[24px] py-0 lg:gap-12 lg:px-48 lg:py-4">
<div className="fixed top-0 z-50 flex h-16 w-full items-center justify-between gap-4 bg-white px-6 py-0 lg:gap-12 lg:px-12 lg:py-4">
<div className="flex items-center gap-4">
<div
onClick={goToMain}
Expand All @@ -55,18 +59,18 @@ export default function Header() {
<nav className="flex flex-row gap-2 text-lg font-bold md:gap-8">
<Link
href="/boards"
className={`hover:text-[--main] ${
className={`hover:text-main ${
pathName.includes("/boards") || pathName.includes("/addBoards")
? "text-[--main]"
? "text-blue-500"
: ""
}`}
>
자유게시판
</Link>
<Link
href="/items"
className={`hover:text-[--main] ${
pathName === "/items" ? "text-[--main]" : ""
className={`hover:text-main ${
pathName === "/items" ? "text-main" : ""
}`}
>
중고마켓
Expand All @@ -85,11 +89,8 @@ export default function Header() {
/>
)}
{isLogoutBoxVisible && (
<div className="absolute right-5 top-14 z-50 rounded-lg bg-white px-4 py-2 shadow-md lg:right-12 lg:top-3">
<button
className="text-gray-700 hover:text-[--main]"
onClick={logout}
>
<div className="absolute right-5 top-14 z-50 rounded-lg bg-white px-4 py-2 shadow-md lg:right-12 lg:top-16">
<button className="text-gray-700 hover:text-main" onClick={logout}>
로그아웃
</button>
</div>
Expand All @@ -98,7 +99,7 @@ export default function Header() {
<button
id="btn_small"
onClick={goToSignin}
className="inline-flex h-[42px] w-[128px] cursor-pointer items-center justify-center rounded-[0.5rem] border-none bg-[--btn1] px-[0.5rem] py-[1.5rem] text-white hover:bg-[--btn2]"
className="inline-flex cursor-pointer items-center justify-center rounded-md border-none bg-main px-6 py-2 text-white hover:bg-btn-2"
>
로그인
</button>
Expand Down
17 changes: 7 additions & 10 deletions components/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useMemo } from "react";

type PaginationProps = {
currentPage: number;
Expand All @@ -19,13 +19,10 @@ export default function Pagination({
onPageChange(currentPage + 1);
};

const pages = [];

for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
// console.log(pages);
// console.log(currentPage, totalPages);
const pages = useMemo(
() => Array.from({ length: totalPages }, (_, i) => i + 1),
[totalPages],
);

Comment on lines +22 to +25
Copy link
Collaborator Author

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.

ㅎㅎㅎ 그쵸. 다음 아티클을 읽어보세요 !

[https://kentcdodds.com/blog/usememo-and-usecallback](useMemo and useCallback)

return (
<div className="mt-8 flex items-center justify-center gap-4">
Expand All @@ -41,8 +38,8 @@ export default function Pagination({
<div
key={i}
className={`flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-gray-300 font-bold ${
currentPage === i ? "bg-[--btn1] text-white" : ""
} hover:bg-[--btn1] hover:text-white`}
currentPage === i ? "bg-btn-1 text-white" : ""
} hover:bg-btn-1 hover:text-white`}
onClick={() => onPageChange(i)}
>
{i}
Expand Down
Loading
Loading