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 #317

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
cec16db
chore: 불필요한 제거
Woolegend Nov 29, 2024
6db2616
feat: 베스트 게시글 이미지 오류 처리
Woolegend Nov 29, 2024
d689db9
refactor: import 경로를 모두 절대 경로로 변경 및 불필요한 impot 제거
Woolegend Nov 29, 2024
1b4525c
style: i 명칭 postIndex로 명확하게 변경
Woolegend Nov 29, 2024
e5dc536
feat: useAsync 커스텀 훅 구현
Woolegend Nov 29, 2024
cb60cfc
feat: 게시글 리스트 useAsync 커스텀 훅 적용
Woolegend Nov 29, 2024
73fcca3
feat: 게시글 정렬 드롭다운 외부 클릭 여부 판단을 위한 커스텀 훅 적용
Woolegend Nov 29, 2024
204ee68
feat: container 컴포넌트 구현
Woolegend Nov 29, 2024
a644810
feat: 글쓰기 페이지 구조 구현
Woolegend Nov 30, 2024
6a4a468
feat: 글쓰기 페이지 디자인 구현
Woolegend Nov 30, 2024
0e0e376
feat: 글쓰기 페이지 이미지 등록 및 이미지 미리보기 구현
Woolegend Nov 30, 2024
840b87b
feat: 글쓰기 유효성 검사 구현
Woolegend Nov 30, 2024
a392885
feat: 이미지 업로드 api 구현
Woolegend Nov 30, 2024
053dd4c
chore: 불필요한 console.log 제거
Woolegend Nov 30, 2024
bdc4f1c
feat: 게시글 등록 api 구현
Woolegend Nov 30, 2024
7fc9edb
feat: 아이디 기반 게시글 읽기 API 구현
Woolegend Nov 30, 2024
a6b3c23
feat: 게시글 상세 페이지 생성
Woolegend Nov 30, 2024
c8b3642
feat: 게시글 상세 페이지 헤더 및 메인 구현
Woolegend Nov 30, 2024
608d74d
feat: 게시글 상세 페이지 댓글 입력 폼 구현
Woolegend Nov 30, 2024
3ad4d85
feat: 게시글 ID 기반 댓글 목록 일기 API 구현
Woolegend Nov 30, 2024
5e4c6b1
feat: 게시글의 댓글 목록 표기 구현
Woolegend Nov 30, 2024
e136570
feat: 댓글 폼 유효성 검사 구현
Woolegend Nov 30, 2024
95f7937
feat: 게시글 상세 페이지 뒤로가기 버튼 구현
Woolegend Nov 30, 2024
407db67
feat: 로그인 및 회원가입 페이지 구조 구현
Woolegend Dec 1, 2024
cbcc1c3
feat: 회원가입 API 구현 및 기능 구현
Woolegend Dec 1, 2024
da2a0a2
feat: 폼 이벤트 제어
Woolegend Dec 1, 2024
fa30cd0
feat: 네비게이션 개선
Woolegend Dec 1, 2024
26a90e6
feat: 로그인 및 회원가입 페이지 디자인 개선
Woolegend Dec 1, 2024
37a25ae
feat: 로그인 정보 세션 스토리지 저장 구현
Woolegend Dec 1, 2024
1f1fb91
feat: 헤더에 accessToken 삽입 함수 구현
Woolegend Dec 2, 2024
8277087
feat: 로그인 시 헤더에 토큰 삽입
Woolegend Dec 2, 2024
2926a1d
feat: 게시글 생성 구현
Woolegend Dec 2, 2024
70c75e1
refactor: 네비게이션 표기 로직 변경 및 불필요 파일 제거
Woolegend Dec 2, 2024
3c559ba
feat: accessToken 갱신 API 구현
Woolegend Dec 2, 2024
51ad049
feat: ISO 시간 경과 읽기 함수 구현
Woolegend Dec 2, 2024
9e37071
feat: 로그인 로그아웃 유틸리티 함수 구현
Woolegend Dec 2, 2024
27ab728
feat: refreshToken 갱신 함수 구현
Woolegend Dec 2, 2024
6d3722f
refactor: accessToken 갱신 함수 개선 및 게시글 댓글 입력 기능 개선
Woolegend Dec 2, 2024
f44da17
feat: 인증이 필요한 API 사용 이전에 토큰 갱신 기능 구현
Woolegend Dec 2, 2024
7b63704
refactor: 페이지 이동 시 토큰 갱신 기능 개선
Woolegend Dec 2, 2024
eaa5c6a
_redirects 파일을 사용해 배포 시 페이지 라우팅 구현
Woolegend Dec 3, 2024
5ce6d0c
fix: _redirects 경로 수정
Woolegend Dec 3, 2024
774a70f
feat: next.config.js redirects 설정 추가
Woolegend Dec 3, 2024
6023f74
fix: 동작하지 않는 _redirects 파일 제거
Woolegend Dec 3, 2024
c5c2007
fix: redirects 규칙 변경
Woolegend Dec 3, 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
32 changes: 30 additions & 2 deletions api/article.api.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ArticleList } from "@/types/Article.type";
import { Article, ArticleList } from "@/types/Article.type";
import axios from "./axios";

interface GetArticleListParams {
Expand Down Expand Up @@ -27,5 +27,33 @@ async function getArticleList({
return response.data;
}

export { getArticleList };
async function getArticle({ id }: { id: number }): Promise<Article> {
const response = await axios.get(`/articles/${id}`);
return response.data;
}
Comment on lines +31 to +32
Copy link
Collaborator

Choose a reason for hiding this comment

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

아래처럼 응답 타입에 제너릭을 추가할 수 있습니다.

const { data } = await axios.get<Article>(`/articles/${id}`);
return data;


interface PostArticle {
image?: string | undefined;
content: string;
title: string;
}

async function postArticle({
image,
content,
title,
}: PostArticle): Promise<Article> {
const response = await axios({
method: "post",
url: "/articles",
data: {
content,
title,
image,
},
});
return response.data;
Comment on lines +46 to +54
Copy link
Collaborator

Choose a reason for hiding this comment

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

axios 사용할 때 현재 처럼 설정 값을 객체 통해 지정하고 있는데, 아래 처럼 메서드를 직접 사용하는 방식이 보다 간결하고 명확해서 변경하면 좋겠습니다. 😸
기능적 차이는 없어요~

await axios.post('/articles', params);

}

export { getArticleList, getArticle, postArticle };
export type { GetArticleListParams, OrderBy };
68 changes: 68 additions & 0 deletions api/auth.api.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { User } from "@/types/User.type";
import axios from "./axios";

interface SignUpParams {
email: string;
nickname: string;
password: string;
passwordConfirmation: string;
}

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

interface AuthResponse {
accessToken: string;
refreshToken: string;
user: User;
}

async function postSignUp({
email,
nickname,
password,
passwordConfirmation,
}: SignUpParams): Promise<AuthResponse> {
const response = await axios({
method: "post",
url: "/auth/signUp",
Copy link
Collaborator

Choose a reason for hiding this comment

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

이러한 URL은 contants 디렉토리 내 api.ts 파일을 하나 만들어서 상수로 관리하면 좋겠어요.

url: AUTH_SIGN_UP_URL,

data: {
email,
nickname,
password,
passwordConfirmation,
},
});
return response.data;
}

async function postSignIn({
email,
password,
}: SignInParams): Promise<AuthResponse> {
const response = await axios({
method: "post",
url: "/auth/signIn",
data: {
email,
password,
},
});
return response.data;
}

async function postRefresh(refreshToken: string) {
const response = await axios({
method: "post",
url: "/auth/refresh-token",
data: {
refreshToken,
},
});
return response.data;
}

export { postSignUp, postSignIn, postRefresh };
export type { SignUpParams, SignInParams, AuthResponse };
9 changes: 0 additions & 9 deletions api/axios.js

This file was deleted.

27 changes: 27 additions & 0 deletions api/axios.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import axios from "axios";

const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;

const instance = axios.create({
baseURL: BASE_URL,
headers: {
"Content-Type": "application/json",
},
});

let interceptorId: any;

export const setInstanceHeaders = (token?: string) => {
const value = token ? `Bearer ${token}` : undefined;
// 기존 인터셉터 제거
if (interceptorId !== undefined) {
instance.interceptors.request.eject(interceptorId);
}

// 새로운 인터셉터 추가
interceptorId = instance.interceptors.request.use((config) => {
config.headers["Authorization"] = value;
return config;
});
Comment on lines +16 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

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

오호 interceptors 아이디어 좋습니다. 👍

};
export default instance;
43 changes: 43 additions & 0 deletions api/comment.api.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import axios from "./axios";

interface GetCommentListByArticleId {
articleId: number;
limit: number;
cursor?: number;
}

async function getCommentListByArticleId({
articleId,
limit,
cursor,
}: GetCommentListByArticleId) {
const response = await axios.get(`/articles/${articleId}/comments`, {
params: {
limit,
cursor,
},
});
return response.data;
}
Comment on lines +3 to +21
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기서 GetCommentListByArticleId 값이 지역 타입이기도 하고, 타입이라 예상되지만 이처럼 PascalCase 로 네이밍이 되게되면 혼란이 야기될 수 있어서 타입 혹은 인터페이스 뒤에 props 붙이는 방법도 있습니다. 즉 GetCommentListByArticleIdProps 이처럼요.


interface PostCommentByArticleId {
articleId: number;
content: string;
}

async function postCommentByArticleId({
articleId,
content,
}: PostCommentByArticleId) {
const response = await axios({
method: "post",
url: `/articles/${articleId}/comments`,
data: {
content,
},
});
return response.data;
}

export { getCommentListByArticleId, postCommentByArticleId };
export type { GetCommentListByArticleId, PostCommentByArticleId };
Comment on lines +42 to +43
Copy link
Collaborator

Choose a reason for hiding this comment

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

정답은 없지만, get / post / delete / update 등을 함수명에 추가하는 방법은 좋습니다. 현업 사례로 기준을 잡고 말씀드리면 API를 요청하는 부분은 fetchOOOO 통일하곤 해요. 즉 fetchArticles 가 되겠습니다. 또한 아래처럼 도메인 중심 네이밍 방법도 있는데요. 선택하셔서 수정하면 지금 보다 더 좋은 네이밍과 유연한 설계가 될 것 같아요.

const articles = {
  list: () => axios.get('/articles'),
  detail: (id: string) => axios.get(`/articles/${id}`),
  create: (data: ArticleData) => axios.post('/articles', data),
  update: (id: string, data: ArticleData) => axios.put(`/articles/${id}`, data),
  delete: (id: string) => axios.delete(`/articles/${id}`)
};

10 changes: 10 additions & 0 deletions api/image.api.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import axios from "./axios";

async function postImage(file: File): Promise<string> {
const response = await axios.post("/images/upload", {
data: { image: file },
});
return response.data.url;
}

export { postImage };
27 changes: 8 additions & 19 deletions components/BestPostBoard.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { Article, ArticleList } from "@/types/Article.type";
import styles from "./BestPostBoard.module.css";
import Image from "next/image";
import formatDate from "../lib/formatDate";
import formatDate from "@/lib/formatDate";
import { useDeviceType } from "@/contexts/DeviceTypeContext";
import { useState } from "react";
import ImageSafe from "./ImageSafe";
import Link from "next/link";

const PAGE_SIZE = {
desktop: 3,
tablet: 2,
mobile: 1,
};

const IMAGE_PLACEHOLDER = "/images/landscape-placeholder.svg";

export default function BestPostBoard({ articles }: { articles: ArticleList }) {
const deviceType = useDeviceType();

Expand All @@ -22,8 +21,8 @@ export default function BestPostBoard({ articles }: { articles: ArticleList }) {
<h2 className={styles.BoardTitle}>베스트 게시글</h2>
</header>
<div className={styles.PostItemList}>
{articles.list.map((article, i) => {
if (i < PAGE_SIZE[deviceType]) {
{articles.list.map((article, postIndex) => {
if (postIndex < PAGE_SIZE[deviceType]) {
return <PostItem key={article.id} article={article} />;
}
})}
Expand All @@ -33,12 +32,10 @@ export default function BestPostBoard({ articles }: { articles: ArticleList }) {
}

function PostItem({ article }: { article: Article }) {
const [imgSrc, setImgSrc] = useState(article.image || IMAGE_PLACEHOLDER);

const createdAt = formatDate(article.createdAt);

return (
<div className={styles.Item}>
<Link className={styles.Item} href={`/board/${article.id}`}>
<div className={styles.badge}>
<div className={styles.medal}>
<Image fill src="/images/ic_medal.svg" alt="베스트" />
Expand All @@ -48,15 +45,7 @@ function PostItem({ article }: { article: Article }) {
<div className={styles.main}>
<h3 className={styles.title}>{article.title}</h3>
<div className={styles.preview}>
<Image
fill
src={article.image}
alt={article.title}
style={{
objectFit: "cover",
}}
onError={() => setImgSrc(IMAGE_PLACEHOLDER)}
/>
<ImageSafe src={article.image} alt={article.title} />
</div>
</div>
<div className={styles.util}>
Expand All @@ -69,6 +58,6 @@ function PostItem({ article }: { article: Article }) {
</div>
<span>{createdAt}</span>
</div>
</div>
</Link>
);
}
5 changes: 5 additions & 0 deletions components/Container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ReactNode } from "react";

export default function Container({ children }: { children: ReactNode }) {
return <div className="container">{children}</div>;
}
5 changes: 0 additions & 5 deletions components/ImageSafe.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import getConfig from "next/config";
import Image from "next/image";
import { useState, useEffect } from "react";

Expand All @@ -13,16 +12,12 @@ export default function ImageSafe({ src, alt }: { src: string; alt: string }) {
const res = await fetch(
"/api/check-image?url=" + encodeURIComponent(src)
);
console.log(res);
if (res.ok) {
setImageSrc(src);
} else {
console.error("Image source not configured in next.config.js");
console.log(res);
setImageSrc(IMAGE_PLACEHOLDER);
}
} catch (error) {
console.error("Error checking image configuration:", error);
setImageSrc(IMAGE_PLACEHOLDER);
}
};
Expand Down
9 changes: 8 additions & 1 deletion components/Navigation.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
gap: 10px;
}

.logo .icon {
.logoIcon {
position: relative;
width: 40px;
height: 40px;
}

.logo .text {
Expand Down Expand Up @@ -56,6 +58,11 @@
.tabList .tab.current {
color: var(--blue);
}
.profileIcon {
position: relative;
width: 40px;
height: 40px;
}

@media screen and (min-width: 1600px) {
.Navigation {
Expand Down
16 changes: 9 additions & 7 deletions components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Image from "next/image";
import styles from "./Navigation.module.css";
import Link from "next/link";

Expand All @@ -17,11 +18,10 @@ function Navigation() {
<nav className={styles.Navigation}>
<div className={styles.wrap}>
<Link href="/" className={styles.logo}>
<img
className={styles.icon}
src="./images/ic_logo.svg"
alt="판다마켓 아이콘 로고"
/>
<div className={styles.logoIcon}>
<Image fill src="./images/ic_logo.svg" alt="판다마켓 아이콘 로고" />
</div>

<span className={styles.text}>판다마켓</span>
</Link>
<ul className={styles.tabList}>
Expand All @@ -31,8 +31,10 @@ function Navigation() {
</li>
))}
</ul>
<Link className={styles.profile} href="/">
<img src="/images/profile.svg" alt="유저 프로필" />
<Link className={styles.profile} href="/signin">
<div className={styles.profileIcon}>
<Image fill src="/images/profile.svg" alt="유저 프로필" />
</div>
</Link>
</div>
</nav>
Expand Down
Loading
Loading