-
Notifications
You must be signed in to change notification settings - Fork 37
[남기연] sprint10 #320
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
The head ref may contain hidden characters: "Next-\uB0A8\uAE30\uC5F0-sprint10"
[남기연] sprint10 #320
Changes from all commits
0963b02
31fad4b
e93ce74
5bf33fb
0772aa2
4bf1364
6bb9f1f
b27456b
050fc19
e997f01
36655de
92b8847
e3e762b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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} | ||
| /> | ||
| ); | ||
| } |
| 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HTTP status code 400이상의 응답에 대한 에러 핸들링을 여기서 해주는데요 |
||
| const result = await response.json(); | ||
| console.log("Success:", result); | ||
|
Comment on lines
+13
to
+14
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요청 성공했는데 아무런 데이터를 반환해주지 않고 있어요.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 우리 멘토링때 이야기했던 내용중, 이 관점에서, 우리가 작업한 이 함수도 어떤 범위에서 보면 하나의 완성된 프로그램입니다. 입력값은 formadata이고, 우리가 작성한 알고리즘을 실행시켜 원하는 결과물을 반환해주어야 하는데, 이 부분 한번 염두에 두고 리팩토링 해볼까요? |
||
| } catch (error) { | ||
| console.error("게시글 생성 실패", error); | ||
| } | ||
| } | ||
| 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| 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}`); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋습니다. |
||
| } | ||
| 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", | ||
|
|
@@ -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(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}`); | ||
|
|
||
| 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}`); | ||
| } | ||
| } | ||
| 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; | ||
| } |
| 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({ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이제 React는 콤포넌트 기반으로 구획을 나누는데요, |
||
| 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> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| .form { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 24px; | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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; | ||
| } |
There was a problem hiding this comment.
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 콤포넌트를 만ㄷ르어 관리하기도 한답니다.
이미지를 그대로 가져와 쓰는 방법 말구, 이런 데이터가 없음을 표현하기 위한 콤포넌트를 한번 작업해보면 어떨까요?