-
Notifications
You must be signed in to change notification settings - Fork 40
[강수민] Sprint12 #344
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-\uAC15\uC218\uBBFC-sprint12"
[강수민] Sprint12 #344
Changes from all commits
19d5742
8c6a873
920dbd0
e49b99b
03cd4e4
6becae0
df8f6e3
3f29ac2
a1ee671
e9851cc
91cf6c8
cbc3c9f
0c683d2
b3b0d8a
9c01259
4fbae12
269bc86
7109177
671f2e5
53b0b4d
89ac092
2cfab97
ca7e92b
de0ea8c
66c7c1b
710b677
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 |
|---|---|---|
|
|
@@ -21,7 +21,9 @@ function Header() { | |
| const handleLogout = () => { | ||
| localStorage.removeItem("accessToken"); | ||
| localStorage.removeItem("refreshToken"); | ||
| localStorage.removeItem("userId"); | ||
|
Comment on lines
22
to
+24
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. localStorage의 키 값들도 상수로 관리해주셔도 좋습니다. |
||
| setHasToken(false); | ||
| router.push("/"); | ||
| }; | ||
|
|
||
| return ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,69 +3,212 @@ import { formatDate } from "@/utils/formatDate"; | |
| import ImageProduct from "../items/ImageProduct"; | ||
| import ProfileIcon from "../Icons/ProfileIcon"; | ||
| import HeartIcon from "../Icons/HeartIcon"; | ||
| import DropDownInquiry from "./DropDownInquiry"; | ||
| import { FormEvent, useEffect, useReducer, useState } from "react"; | ||
| import { useRouter } from "next/router"; | ||
| import useModal from "@/hooks/useModal"; | ||
| import Modal from "@/components/modal/Modal"; | ||
| import ImgFileInput from "../addItem/ImgFileInput"; | ||
| import { ProductInputReducer } from "@/reducers/useProductReducer"; | ||
| import TextInput from "../addItem/TextInput"; | ||
| import Description from "../addItem/Description"; | ||
| import PriceInput from "../addItem/PriceInput"; | ||
| import TagInput from "../addItem/TagInput"; | ||
| import { updateProduct } from "@/pages/api/productApi"; | ||
| import queryClient from "@/lib/queryClient"; | ||
|
|
||
| interface DetailProductProps { | ||
| id: number; | ||
| name: string; | ||
| description: string; | ||
| images: string[]; | ||
| price: number; | ||
| favoriteCount: number; | ||
| tags: string[]; | ||
| ownerId: number; | ||
| ownerNickname: string; | ||
| updatedAt: string; | ||
| isFavorite?: boolean; | ||
| onDelete: (id: string) => void; | ||
| } | ||
|
|
||
| function DetailProduct({ | ||
| id, | ||
| name, | ||
| description, | ||
| images, | ||
| price, | ||
| favoriteCount, | ||
| tags, | ||
| ownerId, | ||
| ownerNickname, | ||
| updatedAt, | ||
| isFavorite, | ||
| onDelete, | ||
| }: DetailProductProps) { | ||
| const router = useRouter(); | ||
| const [isEditing, setIsEditing] = useState(false); // 수정 상태를 할당할 state | ||
| const userId = localStorage.getItem("userId"); | ||
| const { isOpen, openModal, closeModal } = useModal(); | ||
| const [isFormValid, setIsFormValid] = useState(false); | ||
| const [originalData, setOriginalData] = useState({ | ||
| images, | ||
| name, | ||
| description, | ||
| price, | ||
| tags, | ||
| }); | ||
| const [userInput, dispatch] = useReducer(ProductInputReducer, originalData); | ||
|
|
||
| const { | ||
| images: u_images, | ||
| name: u_name, | ||
| description: u_description, | ||
| price: u_price, | ||
| tags: u_tags, | ||
|
Comment on lines
+64
to
+68
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. 여기도 카멜 케이스 그대로 활용해주셔도 괜찮습니다. uImages, uName 이런식으로요! |
||
| } = userInput; | ||
|
|
||
| const handleCloseModal = () => { | ||
| setIsEditing(false); | ||
| closeModal(); | ||
| dispatch({ type: "RESET_INPUT", payload: originalData }); // 원래 데이터로 복원 | ||
| }; | ||
|
|
||
| const handleSubmit = async (e: FormEvent) => { | ||
| e.preventDefault(); | ||
|
|
||
| if (isFormValid) { | ||
| const imageChange = u_images[0] !== originalData.images[0]; | ||
|
|
||
| const { id: productId } = await updateProduct({ | ||
| id: String(id), | ||
| content: userInput, | ||
| imageChange: imageChange, | ||
| }); | ||
|
|
||
| setOriginalData(userInput); | ||
| queryClient.invalidateQueries({ | ||
| queryKey: ["product", String(productId)], | ||
| }); | ||
| queryClient.invalidateQueries({ queryKey: ["products"], exact: false }); | ||
| dispatch({ type: "RESET_INPUT", payload: userInput }); | ||
| setIsEditing(false); | ||
| closeModal(); | ||
| } | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| const isChanged = | ||
| u_images !== originalData.images || | ||
| u_name !== originalData.name || | ||
| u_description !== originalData.description || | ||
| u_price !== originalData.price || | ||
| JSON.stringify(u_tags) !== JSON.stringify(originalData.tags); | ||
|
|
||
| const hasRequiredFields = | ||
| u_name.trim() !== "" && | ||
| u_description.trim() !== "" && | ||
| u_price > 0 && | ||
| u_tags.length > 0; | ||
|
|
||
| setIsFormValid(isChanged && hasRequiredFields); | ||
| }, [u_images, u_name, u_description, u_price, u_tags, originalData]); | ||
|
|
||
| return ( | ||
| <div className='product-detail-contents'> | ||
| <ImageProduct images={images} name={name} /> | ||
| <div className='desc-wrap'> | ||
| <h2 className='detail-name'>{name}</h2> | ||
| <div className='detail-price'>{formatPriceToKRW(price)}</div> | ||
| <div className='detail-description-wrap'> | ||
| <div className='detail-description-title'>상품 소개</div> | ||
| <p className='detail-description'>{description}</p> | ||
| </div> | ||
| <div className='detail-tag-wrap'> | ||
| <div className='detail-tag-title'>상품 태그</div> | ||
| <div className='detail-tags'> | ||
| {tags.map((tag) => ( | ||
| <span key={tag} className='detail-tag'> | ||
| #{tag} | ||
| </span> | ||
| ))} | ||
| <> | ||
| <div className='product-detail-contents'> | ||
| {!isEditing && String(ownerId) === userId && ( | ||
| <DropDownInquiry | ||
| onEdit={() => { | ||
| openModal(); | ||
| setIsEditing(true); | ||
| }} | ||
| onDelete={() => { | ||
| onDelete(String(id)); | ||
| router.push("/items"); | ||
| }} | ||
| /> | ||
| )} | ||
| <ImageProduct images={images} name={name} /> | ||
| <div className='desc-wrap'> | ||
| <h2 className='detail-name'>{name}</h2> | ||
| <div className='detail-price'>{formatPriceToKRW(price)}</div> | ||
| <div className='detail-description-wrap'> | ||
| <div className='detail-description-title'>상품 소개</div> | ||
| <p className='detail-description'>{description}</p> | ||
| </div> | ||
| </div> | ||
| <div className='detail-footer'> | ||
| <div className='owner-wrap'> | ||
| <div className='owner-icon'> | ||
| <ProfileIcon /> | ||
| </div> | ||
| <div className='owner-desc'> | ||
| <div className='owner-name'>{ownerNickname}</div> | ||
| <div className='date-update'>{formatDate(updatedAt)}</div> | ||
| <div className='detail-tag-wrap'> | ||
| <div className='detail-tag-title'>상품 태그</div> | ||
| <div className='detail-tags'> | ||
| {tags.map((tag) => ( | ||
| <span key={tag} className='detail-tag'> | ||
| #{tag} | ||
| </span> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| <button className='btn-favorite'> | ||
| <div> | ||
| <HeartIcon isFavorite={isFavorite} /> | ||
| <div className='detail-footer'> | ||
| <div className='owner-wrap'> | ||
| <div className='owner-icon'> | ||
| <ProfileIcon /> | ||
| </div> | ||
| <div className='owner-desc'> | ||
| <div className='owner-name'>{ownerNickname}</div> | ||
| <div className='date-update'>{formatDate(updatedAt)}</div> | ||
| </div> | ||
| </div> | ||
| <span>{favoriteCount}</span> | ||
| </button> | ||
| <button className='btn-favorite'> | ||
| <div> | ||
| <HeartIcon isFavorite={isFavorite} /> | ||
| </div> | ||
| <span>{favoriteCount}</span> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <Modal isOpen={isOpen} closeModal={handleCloseModal}> | ||
|
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. 모달 컴포넌트 내부의 코드가 은근히 길어져서 따로 컴포넌트로 만드셔서 사용해주셔도 좋습니다. |
||
| <form className='edit-product-form' onSubmit={handleSubmit}> | ||
| <ImgFileInput name='image' images={u_images} dispatch={dispatch}> | ||
| 상품 이미지 | ||
| </ImgFileInput> | ||
| <TextInput | ||
| name='name' | ||
| value={u_name} | ||
| placeholder='상품명을 입력해주세요' | ||
| dispatch={dispatch} | ||
| > | ||
| 상품명 | ||
| </TextInput> | ||
| <Description | ||
| name='description' | ||
| value={u_description} | ||
| placeholder='상품 소개를 입력해주세요' | ||
| dispatch={dispatch} | ||
| > | ||
| 상품 소개 | ||
| </Description> | ||
| <PriceInput name='price' value={u_price} dispatch={dispatch}> | ||
| 판매가격 | ||
| </PriceInput> | ||
| <TagInput | ||
| name='tags' | ||
| tags={u_tags} | ||
| placeholder='태그를 입력해주세요' | ||
| dispatch={dispatch} | ||
| > | ||
| 태그 | ||
| </TagInput> | ||
| <div className='button-wrap'> | ||
| <button type='button' onClick={handleCloseModal}> | ||
| 취소 | ||
| </button> | ||
| <button type='submit' disabled={!isFormValid}> | ||
| 수정 | ||
| </button> | ||
| </div> | ||
| </form> | ||
| </Modal> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
|
|
||
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.
imgUrl을 사용하는 곳이 한 군데 뿐이라면 따로 변수를 만들지 않고
setImg(files[0])이런식으로 작성하셔도 됩니다.