-
Notifications
You must be signed in to change notification settings - Fork 40
[이준희] sprint11 #324
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-\uC774\uC900\uD76C-sprint11"
[이준희] sprint11 #324
Changes from all commits
85dae1f
97dc7a2
ef4cf21
5da7648
dd54a41
66378a8
4748d98
6df0240
8d30eab
353aa9f
24154b8
1f4c293
af29653
84958c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| import { useState } from "react"; | ||
| import ImgInput from "@/components/ImageInput"; | ||
| import resetImg from "@/public/svgs/ic_X.svg"; | ||
| import { useRouter } from "next/router"; | ||
| import { AddItemFormProps } from "@/types/commontypes"; | ||
| import Image from "next/image"; | ||
| import styles from "@/styles/additem.module.css"; | ||
|
|
||
| const INITIAL_VALUE = { | ||
| name: "", | ||
| favoriteCount: 0, | ||
| description: "", | ||
| price: 0, | ||
| images: null, | ||
| tags: [], | ||
| }; | ||
|
|
||
| export default function AddItemForm({ | ||
| initialValues = INITIAL_VALUE, | ||
| initialPreview, | ||
| onSubmit, | ||
| onSubmitSuccess, | ||
| }: AddItemFormProps) { | ||
| const router = useRouter(); | ||
| const [values, setValues] = useState(initialValues); | ||
| const [submittingError, setSubmittingError] = useState<Error | null>(null); | ||
| const [tagInput, setTagInput] = useState(""); | ||
|
|
||
| // 유효성 검사 | ||
| const isValidForm = | ||
| values.name?.trim() !== "" && | ||
| values.description?.trim() !== "" && | ||
| values.price > 0 && | ||
| values.tags.length > 0; | ||
|
|
||
| const handleChange = (name: string, value: any) => { | ||
| setValues((initialValues) => ({ | ||
| ...initialValues, | ||
| [name]: value, | ||
| })); | ||
| }; | ||
|
|
||
| const handleInputChange = ( | ||
| e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | ||
| ) => { | ||
| const { name, value } = e.target; | ||
| handleChange(name, value); | ||
| }; | ||
|
|
||
| const handleFileChange = (name: string, file: File | null) => { | ||
| handleChange(name, file); | ||
| }; | ||
|
|
||
| const handleTagInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| setTagInput(e.target.value); | ||
| }; | ||
|
|
||
| const handleTagAdd = () => { | ||
| if (tagInput.trim() && !values.tags.includes(tagInput.trim())) { | ||
| handleChange("tags", [...values.tags, tagInput.trim()]); | ||
| setTagInput(""); | ||
| } | ||
| }; | ||
|
|
||
| const handleTagKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { | ||
| if (e.key === "Enter") { | ||
| e.preventDefault(); | ||
| handleTagAdd(); | ||
| } | ||
| }; | ||
|
|
||
| const handleTagRemove = (tagToRemove: string) => { | ||
| handleChange( | ||
| "tags", | ||
| values.tags.filter((tag) => tag !== tagToRemove) | ||
| ); | ||
| }; | ||
|
|
||
| const handleSubmit = async (e: React.FormEvent) => { | ||
| e.preventDefault(); | ||
| const formData = new FormData(); | ||
| formData.append("name", values.name); | ||
| formData.append("favorite", values.favoriteCount.toString()); | ||
| formData.append("description", values.description); | ||
| formData.append("price", values.price.toString()); | ||
| if (values.images) { | ||
| values.images.forEach((image, index) => { | ||
| formData.append(`images[${index}]`, image); | ||
| }); | ||
| } | ||
| formData.append("tags", JSON.stringify(values.tags)); | ||
|
Comment on lines
+80
to
+91
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. 로직 간에 개행으로 구분해주시면 가독성에 도움이 될 것 같습니다. |
||
|
|
||
| const result = await onSubmit(formData); | ||
| if (!result) return; | ||
|
|
||
| const { review } = result; | ||
| setValues(INITIAL_VALUE); | ||
| onSubmitSuccess(review); | ||
|
|
||
| router.push("/items"); | ||
| }; | ||
|
|
||
| return ( | ||
| <form | ||
| onSubmit={handleSubmit} | ||
| className={styles.add_item_form_total_container} | ||
| > | ||
| <div className={styles.add_item_form_header}> | ||
| <div className={styles.add_item_form_title}>상품 등록하기</div> | ||
| <button | ||
| type="submit" | ||
| className={styles.add_item_submit_button} | ||
| disabled={!isValidForm || !!submittingError} | ||
| > | ||
| 등록 | ||
| </button> | ||
| </div> | ||
| <div className={styles.add_item_form_container}> | ||
| <div className={styles.add_item_img_container}> | ||
| <div className={styles.add_item_img_title}>상품 이미지</div> | ||
| <ImgInput | ||
| className={styles.add_item_img_preview} | ||
| name="imgFile" | ||
| value={values.images ? values.images[0] : null} | ||
| initialPreview={initialPreview} | ||
| onChange={handleFileChange} | ||
| /> | ||
| </div> | ||
| <div className={styles.add_item_name_container}> | ||
| <div className={styles.add_item_name_title}>상품명</div> | ||
| <input | ||
| className={styles.add_item_name_input} | ||
| name="name" | ||
| value={values.name} | ||
| onChange={handleInputChange} | ||
| placeholder="상품명을 입력해주세요" | ||
| /> | ||
| </div> | ||
| <div className={styles.add_item_content_container}> | ||
| <div className={styles.add_item_content_title}>상품 소개</div> | ||
| <textarea | ||
| className={styles.add_item_content_textarea} | ||
| name="description" | ||
| value={values.description} | ||
| onChange={handleInputChange} | ||
| placeholder="상품 소개를 입력해주세요" | ||
| /> | ||
| </div> | ||
| <div className={styles.add_item_price_container}> | ||
| <div className={styles.add_item_price_title}>판매가격</div> | ||
| <input | ||
| className={styles.add_item_price_input} | ||
| type="number" | ||
| name="price" | ||
| value={values.price} | ||
| onChange={handleInputChange} | ||
| placeholder="판매 가격을 입력해주세요" | ||
| /> | ||
| </div> | ||
| <div className={styles.add_item_tags_container}> | ||
| <div className={styles.add_item_tags_title}>태그</div> | ||
| <input | ||
| className={styles.add_item_tag_input} | ||
| type="text" | ||
| value={tagInput} | ||
| onChange={handleTagInputChange} | ||
| onKeyDown={handleTagKeyDown} | ||
| placeholder="태그를 입력해주세요" | ||
| /> | ||
| <div className={styles.add_item_tags_list}> | ||
| {values.tags.map((tag) => ( | ||
| <div key={tag} className={styles.add_item_tag}> | ||
| #{tag} | ||
| <button | ||
| type="button" | ||
| className={styles.add_item_tag_remove_button} | ||
| onClick={() => handleTagRemove(tag)} | ||
| > | ||
| <Image src={resetImg} alt="제거" /> | ||
| </button> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </form> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import heart from "@/public/svgs/ic_heart (1).svg"; | |
| import defaultImage from "@/public/pngs/noImage.png"; | ||
| import searchIcon from "@/public/svgs/ic_search.svg"; | ||
| import { getArticles } from "@/lib/api"; | ||
| import debounce from "@/lib/utils/debounce"; | ||
|
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. debounce 유틸 함수로 만들어주신거 좋네요. |
||
|
|
||
| export default function AllArticles() { | ||
| const [articles, setArticles] = useState<Article[]>([]); | ||
|
|
@@ -25,9 +26,9 @@ export default function AllArticles() { | |
|
|
||
| const data = await getArticles({ | ||
| orderBy: sortOrder, | ||
| page: page, | ||
| page, | ||
| pageSize: 10, | ||
| keyword: keyword, | ||
| keyword, | ||
| }); | ||
|
|
||
| if (page === 1) { | ||
|
|
@@ -57,24 +58,22 @@ export default function AllArticles() { | |
| }, [page]); | ||
|
|
||
| useEffect(() => { | ||
| let debounceTimer: NodeJS.Timeout; | ||
|
|
||
| const handleScroll = () => { | ||
| clearTimeout(debounceTimer); | ||
| debounceTimer = setTimeout(() => { | ||
| if ( | ||
| window.innerHeight + document.documentElement.scrollTop >= | ||
| document.documentElement.offsetHeight - 50 && | ||
| !isFetching && | ||
| hasMore | ||
| ) { | ||
| setPage((prev) => prev + 1); | ||
| } | ||
| }, 200); | ||
| }; | ||
| const handleScroll = debounce(() => { | ||
| if ( | ||
| window.innerHeight + document.documentElement.scrollTop >= | ||
| document.documentElement.offsetHeight - 50 && | ||
| !isFetching && | ||
| hasMore | ||
| ) { | ||
| setPage((prev) => prev + 1); | ||
| } | ||
| }, 200); | ||
|
|
||
| window.addEventListener("scroll", handleScroll); | ||
| return () => window.removeEventListener("scroll", handleScroll); | ||
|
|
||
| return () => { | ||
| window.removeEventListener("scroll", handleScroll); | ||
| }; | ||
| }, [isFetching, hasMore]); | ||
|
|
||
| const toggleDropdown = () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,18 +6,7 @@ import { Product } from "@/types/commontypes"; | |
| import styles from "@/styles/items.module.css"; | ||
| import searchIcon from "@/public/svgs/ic_search.svg"; | ||
| import Image from "next/image"; | ||
|
|
||
| const getPageSize = () => { | ||
| if (typeof window === "undefined") return 10; | ||
| const width = window.innerWidth; | ||
| if (width < 768) { | ||
| return 4; | ||
| } else if (width < 1280) { | ||
| return 6; | ||
| } else { | ||
| return 10; | ||
| } | ||
| }; | ||
| import getPageSize from "@/lib/utils/getPageSize"; | ||
|
|
||
| function AllItems() { | ||
| const [orderBy, setOrderBy] = useState<string>("recent"); | ||
|
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. string 타입 대신에 들어가는 문자열을 타입으로 사용해보는 것은 어떨까요? |
||
|
|
@@ -26,34 +15,45 @@ function AllItems() { | |
| const [items, setItems] = useState<Product[]>([]); | ||
| const [isDropdown, setIsDropdown] = useState<boolean>(false); | ||
| const [totalPageNum, setTotalPageNum] = useState<number>(0); | ||
| const [keyword, setKeyword] = useState<string>(""); | ||
|
|
||
| useEffect(() => { | ||
| const handleFixSize = () => { | ||
| setPageSize(getPageSize()); | ||
| }; | ||
|
|
||
| const fetchProducts = async ({ | ||
| const fetchProducts = async () => { | ||
| const products = await getProducts({ | ||
| orderBy, | ||
| page, | ||
| pageSize, | ||
| }: { | ||
| orderBy: string; | ||
| page: number; | ||
| pageSize: number; | ||
| }) => { | ||
| const products = await getProducts({ orderBy, page, pageSize }); | ||
| setItems(products.list); | ||
| setTotalPageNum(Math.ceil(products.totalCount / pageSize)); | ||
| keyword: keyword.trim(), | ||
| }); | ||
|
|
||
| setItems(products.list); | ||
| setTotalPageNum(Math.ceil(products.totalCount / pageSize)); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| const handleFixSize = () => { | ||
| setPageSize(getPageSize()); | ||
| }; | ||
|
|
||
| window.addEventListener("resize", handleFixSize); | ||
| fetchProducts({ orderBy, page, pageSize }); | ||
|
|
||
| fetchProducts(); | ||
|
|
||
| return () => { | ||
| window.removeEventListener("resize", handleFixSize); | ||
| }; | ||
| }, [orderBy, page, pageSize]); | ||
|
|
||
| const handleSearchSubmit = () => { | ||
| setPage(1); | ||
| fetchProducts(); | ||
| }; | ||
|
|
||
| const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { | ||
| if (event.key === "Enter") { | ||
| handleSearchSubmit(); | ||
| } | ||
| }; | ||
|
|
||
| const handleNextPage = (newPage: number) => { | ||
| setPage(newPage); | ||
| }; | ||
|
|
@@ -68,23 +68,37 @@ function AllItems() { | |
| setIsDropdown(false); | ||
| }; | ||
|
|
||
| const handleSearchInputChange = ( | ||
| event: React.ChangeEvent<HTMLInputElement> | ||
| ) => { | ||
| setKeyword(event.target.value); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className={styles.all_item_container}> | ||
| <div className={styles.all_item_content}> | ||
| <div className={styles.all_item_header}> | ||
| <div className={styles.all_item_header_front}> | ||
| <div className={styles.all_item_title}>전체 상품</div> | ||
| <div className={styles.all_item_search_container}> | ||
| <Image | ||
| className={styles.all_item_search_icon} | ||
| src={searchIcon} | ||
| alt="돋보기 아이콘" | ||
| width={24} | ||
| height={24} | ||
| /> | ||
| <button | ||
| onClick={handleSearchSubmit} | ||
| className={styles.search_button} | ||
| > | ||
| <Image | ||
| className={styles.all_item_search_icon} | ||
| src={searchIcon} | ||
| alt="돋보기 아이콘" | ||
| width={24} | ||
| height={24} | ||
| /> | ||
|
Comment on lines
+88
to
+94
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. icon은 next/image를 사용하지 않고 써보시는 것도 추천드릴게요. |
||
| </button> | ||
| <input | ||
| className={styles.all_item_search_input} | ||
| placeholder="검색할 상품을 입력해주세요" | ||
| value={keyword} | ||
| onChange={handleSearchInputChange} | ||
| onKeyDown={handleKeyDown} | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
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.
value를 업데이트 할때는 initialValues 보다는 prev나 prevState 같은 단어를 사용해주시는 것이 조금 더 직관적일 것 같습니다.