Skip to content
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
10 changes: 10 additions & 0 deletions next/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"@netlify/plugin-nextjs": "^5.9.3",
"clsx": "^2.1.1",
"next": "15.1.5",
"react": "^19.0.0",
"react-dom": "^19.0.0"
Expand Down
11 changes: 11 additions & 0 deletions next/public/images/comments_null.svg
Copy link
Collaborator

Choose a reason for hiding this comment

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

아마 null placeholder의 경우, 대체적으로 이미지만 아이콘 또는 svg 이미지를 활용하고
그 하위에 달리는 텍스트의 경우, 우리가 코드로 구현하는 상황이 좀 더 많을거에요.

보통 emptyState라는 콤포넌트를 하나 만들어두고, 거기에 보여질 이미지와 텍스트를 동적으로 수정할 수 있는 공통 UI 콤포넌트를 만ㄷ르어 관리하기도 한답니다.

이미지를 그대로 가져와 쓰는 방법 말구, 이런 데이터가 없음을 표현하기 위한 콤포넌트를 한번 작업해보면 어떨까요?

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions next/public/images/ic_X.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions next/public/images/ic_kebab.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions next/public/images/ic_plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions next/src/components/Input.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
border: none;
border-radius: 12px;
background-color: #f3f4f6;
}

.inputWithSearch {
background-image: url("/images/ic_search.svg");
background-repeat: no-repeat;
background-position: 16px center;
Expand Down
21 changes: 18 additions & 3 deletions next/src/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,45 @@
import styles from "./Input.module.css";
import clsx from "clsx";

type InputProps = {
name?: string;
value: string;
placeholder: string;
onChange: (value: string) => void;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onEnter?: () => void;
withSearch?: boolean;
className?: string;
};

export default function Input({
name,
value,
placeholder,
onChange,
onEnter,
withSearch,
className,
...rest
}: InputProps) {
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && onEnter) {
onEnter();
}
};

const inputClassName = clsx(styles.input, className, {
[styles["inputWithSearch"]]: withSearch,
});

return (
<input
className={styles.input}
name={name}
className={inputClassName}
value={value}
placeholder={placeholder}
onChange={(e) => onChange(e.target.value)}
onChange={onChange}
onKeyDown={handleKeyDown}
{...rest}
/>
);
}
2 changes: 1 addition & 1 deletion next/src/components/MainHeader.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
padding: 0px 200px;
border-bottom: 1px solid #dfdfdf;

@media (max-width: 768px) {
@media (max-width: 1024px) {
padding: 0px 24px;
}
}
Expand Down
18 changes: 18 additions & 0 deletions next/src/lib/create-articles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { baseURL } from "@/constants";

export default async function createArticles(formData: FormData) {
console.log(baseURL);
try {
const response = await fetch(`${baseURL}/articles`, {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error("articles를 생성하는데 실패했습니다.");
}
Comment on lines +10 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

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

HTTP status code 400이상의 응답에 대한 에러 핸들링을 여기서 해주는데요
보통 이 경우라면 서버쪽에서 왜 에러로 응답을 제공했는지를 response body에 데이터로 전달을 해줄거에요.
그리고 이 정보를 활용해야 디버깅이 수월할텐데요, 지금 전반적으로 꾸준히 이 정보가 유실되는듯 싶습니다.
따라서 서버에서 반환한 에러 객체를 로깅을 해볼 수 있으면 좋겠어요!

const result = await response.json();
console.log("Success:", result);
Comment on lines +13 to +14
Copy link
Collaborator

Choose a reason for hiding this comment

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

요청 성공했는데 아무런 데이터를 반환해주지 않고 있어요.
사실 데이터가 생성이 되었다면, 해당 데이터를 활용해 UI update를 함께 진행하는 경우가 많은데요,
이를 위한 트리거가 서버에서 제공해준 생성된 엔티티를 반환하거나, true / false를 반환해 성공적으로 생성이 되었는지에 대한 여부정도는 반환해주어야 이 함수를 활용하는 개발자 입장에서 사용하기가 수월하답니다.
하지만, 이 함수는 그런 반환이 없이 오류가 났는지, 성공적이였는지를 판단하기 위해서는 무조건 개발자도구를 열어야만 할거에요.

Copy link
Collaborator

Choose a reason for hiding this comment

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

우리 멘토링때 이야기했던 내용중,
프로그램이란 입력값을 우리가 작성한 코드로 실행해 원하는 결과물을 반환하는 소프트웨어라고 이야기 했었잖아요.

이 관점에서, 우리가 작업한 이 함수도 어떤 범위에서 보면 하나의 완성된 프로그램입니다.

입력값은 formadata이고, 우리가 작성한 알고리즘을 실행시켜 원하는 결과물을 반환해주어야 하는데,
이 관점에서 원하는 결과물이 무엇인지도 판단하기가 어렵고, 그 결과물이 어떤 형태로 반환되어야 하는지에 대한 고려가 되어있지 않아보여요.

이 부분 한번 염두에 두고 리팩토링 해볼까요?

} catch (error) {
console.error("게시글 생성 실패", error);
}
}
25 changes: 25 additions & 0 deletions next/src/lib/create-comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { baseURL } from "@/constants";

export default async function createComment(
articleId: number,
comment: string
) {
try {
const response = await fetch(`${baseURL}/articles/${articleId}/comments`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
comment,
}),
Comment on lines +8 to +15
Copy link
Collaborator

Choose a reason for hiding this comment

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

특정 요소들은 formdata로 주고받고, 어떤 요소들은 application json으로 주고받는데 서버 요구사항을 따른거겠죠?

});
if (!response.ok) {
throw new Error("comment를 생성하는데 실패했습니다.");
}
const result = await response.json();
console.log("Success 댓글:", result);
} catch (error) {
console.error("댓글 생성 실패", error);
}
}
17 changes: 17 additions & 0 deletions next/src/lib/get-article.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { baseURL } from "@/constants";
import { Article } from "@/types";

export default async function getArticle(articleId: number): Promise<Article> {
const url = `${baseURL}/articles/${articleId}`;

try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch, ${response.statusText}`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

statustext는 아마 불충분할거에요 ㅎㅎ

}
const data: Article = await response.json();
return data;
} catch (err) {
throw new Error(`FetchPost Error: ${(err as Error).message}`);
}
Comment on lines +13 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

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

좋습니다.
우리 로직이 성종적으로 동작한다면 객체를 반환하고
오류 발생을 한다면 error raise를 시켜주어, 이 함수를 사용하는 클라이언트가 그에 따른 적절한 케이스 핸들링을 해볼 수 있는 코드가 완성되었어요

}
4 changes: 2 additions & 2 deletions next/src/lib/fetch-posts.ts → next/src/lib/get-articles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { baseURL } from "@/constants";
import { Post, Posts } from "../types";

export default async function fetchPosts({
export default async function getArticles({
page = "1",
pageSize = 3,
orderBy = "recent",
Expand All @@ -20,7 +20,7 @@ export default async function fetchPosts({
if (!response.ok) {
throw new Error(`Failed to fetch, ${response.statusText}`);
}
const data: Posts = await response.json();
const data: Posts<Post> = await response.json();
Copy link
Collaborator

Choose a reason for hiding this comment

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

아 이렇게보단 다음과 같이 해보면 어떤가요

페이지네이션 정보를 포함한 데이터를 반환하는 응답 객체에 대한 타입을 처리하기 위해

interface PaginatedAPIResponse<T> {
  list: T[]
  count: number
  page: number
  ...
}

로 만들어주고,

지금처럼 글 정보를 pagination결과와 묶엇 ㅓ반환하는 api에 대해 이렇게 처리를 해볼 수 있겠죠

const response = await fetch(...)
const data = await response.json() as PaginatedApiResponse<Post>

const { list, page, totalCount, ... } = data

...

return data.list;
} catch (err) {
throw new Error(`FetchPosts Error: ${(err as Error).message}`);
Expand Down
24 changes: 24 additions & 0 deletions next/src/lib/get-comments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { baseURL } from "@/constants";
import { Comments, Comment } from "@/types";

export default async function getComments(
articleId: number,
limit: number = 3
): Promise<Comment[]> {
const params = new URLSearchParams({
limit: String(limit),
});
Comment on lines +8 to +10
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍🏻


try {
const response = await fetch(
`${baseURL}/articles/${articleId}/comments?${params.toString()}`
);
if (!response.ok) {
throw new Error(`Failed to fetch, ${response.statusText}`);
}
const data: Comments<Comment> = await response.json();
return data.list;
} catch (err) {
throw new Error(`FetchComments Error: ${(err as Error).message}`);
}
}
4 changes: 3 additions & 1 deletion next/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AppProps } from "next/app";
import "@/src/styles/globals.css";
import "@/styles/globals.css";
import Head from "next/head";
import MainHeader from "@/components/MainHeader";

export default function App({ Component, pageProps }: AppProps) {
return (
Expand All @@ -9,6 +10,7 @@ export default function App({ Component, pageProps }: AppProps) {
<title>판다마켓</title>
<link rel="icon" href="/images/favicon.svg" />
</Head>
<MainHeader />
<Component {...pageProps} />
</>
);
Expand Down
19 changes: 19 additions & 0 deletions next/src/pages/addboard/components/ContentSection.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.container {
display: flex;
flex-direction: column;
gap: 12px;
}

.title {
font-weight: 700;
font-size: 18px;
color: #1f2937;
}

.textarea {
border: none;
border-radius: 12px;
padding: 16px 24px;
font-size: 16px;
background-color: #f3f4f6;
}
24 changes: 24 additions & 0 deletions next/src/pages/addboard/components/ContentSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import styles from "./ContentSection.module.css";

type ContentSectionProps = {
content: string;
setContent: (value: string) => void;
};

export default function ContentSection({
Copy link
Collaborator

Choose a reason for hiding this comment

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

이제 React는 콤포넌트 기반으로 구획을 나누는데요,
이 관점에서 ~~~Section이라고 이름을 붙이기보다는
좀 더 콤포넌트 이름만으로 어떤 기능과 동작을 상상할 수 있는지 고민을 해보고
콤포넌트 네임을 지어보면 좋을것같아요

content,
setContent,
}: ContentSectionProps) {
return (
<div className={styles.container}>
<h2 className={styles.title}>*내용</h2>
<textarea
value={content}
className={styles.textarea}
rows={13}
placeholder="내용을 입력해주세요"
onChange={(e) => setContent(e.target.value)}
/>
</div>
);
}
5 changes: 5 additions & 0 deletions next/src/pages/addboard/components/FormSection.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.form {
display: flex;
flex-direction: column;
gap: 24px;
}
14 changes: 14 additions & 0 deletions next/src/pages/addboard/components/FormSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import styles from "./FormSection.module.css";
import ContentSection from "./ContentSection";
import ImageSection from "./ImageSection";
import TitleSection from "./TitleSection";

export default function FormSection() {
return (
<form action="" className={styles.form}>
<TitleSection />
<ContentSection />
<ImageSection />
</form>
);
}
52 changes: 52 additions & 0 deletions next/src/pages/addboard/components/ImageSection.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.container {
display: flex;
flex-direction: column;
gap: 12px;
}

.title {
font-weight: 700;
font-size: 18px;
color: #1f2937;
}

.label {
display: flex;
justify-content: center;
align-items: center;
width: 282px;
height: 282px;
border-radius: 12px;
background-color: #f3f4f6;
cursor: pointer;
}

.labelContent {
display: flex;
flex-direction: column;
align-items: center;
}

.imageContainer {
position: relative;
}

.delete {
position: absolute;
top: 5px;
right: 5px;
}

.delete:hover{

}

.span {
font-weight: 400;
font-size: 16px;
color: #9ca3af;
}

.input {
display: none;
}
Loading
Loading