Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
19d5742
[chore]: strictMode 제거, Image 와일드카드 패턴 적용 수정
hpk5802 Jan 7, 2025
8c6a873
[chore]: react-query 추가
hpk5802 Jan 7, 2025
920dbd0
[chore]: react-query-devtools 추가
hpk5802 Jan 7, 2025
e49b99b
[feat]: react-query 초기 설정
hpk5802 Jan 7, 2025
03cd4e4
[feat]: userId를 localStorage에서 관리하도록 추가
hpk5802 Jan 7, 2025
6becae0
[refactor]: 이미지 input 타입 string에서 File로 변경
hpk5802 Jan 7, 2025
df8f6e3
[refactor]: 상품 추가 api 수정
hpk5802 Jan 7, 2025
3f29ac2
[feat]: 상품 등록 성공 시 해당 상품 상세 페이지로 이동
hpk5802 Jan 7, 2025
a1ee671
[refactor]: 상품 관련 api를 Tanstack React Query로 마이그레이션
hpk5802 Jan 7, 2025
e9851cc
[refactor]: 댓글 관련 api를 Tanstack React Query로 마이그레이션
hpk5802 Jan 7, 2025
91cf6c8
[feat]: 댓글 수정 기능 추가
hpk5802 Jan 7, 2025
cbc3c9f
[feat]: 댓글 삭제 api 추가
hpk5802 Jan 7, 2025
0c683d2
[feat]: 댓글 삭제 기능 추가
hpk5802 Jan 7, 2025
b3b0d8a
[feat]: 댓글 작성자만 수정, 삭제 가능하도록 제한
hpk5802 Jan 7, 2025
9c01259
[fix]: ts 타입 에러 수정
hpk5802 Jan 8, 2025
4fbae12
[feat]: 상품 작성자만 수정, 삭제 가능한 dropdown 메뉴 추가
hpk5802 Jan 8, 2025
269bc86
[feat]: 상품 삭제 기능 추가
hpk5802 Jan 8, 2025
7109177
[refactor]: 이미지 업로드 api 수정
hpk5802 Jan 8, 2025
671f2e5
[refactor]: 상품 등록 후 데이터 갱신하도록 수정
hpk5802 Jan 8, 2025
53b0b4d
[refactor]: Dropdown 수정하기 이벤트 핸들러 받도록 수정
hpk5802 Jan 8, 2025
89ac092
[feat]: 공통 모달 추가
hpk5802 Jan 8, 2025
2cfab97
[style]: 상품 수정 모달 스타일 추가
hpk5802 Jan 8, 2025
ca7e92b
[refactor]: useProductReducer 수정 초기화 action 추가
hpk5802 Jan 8, 2025
de0ea8c
[feat]: 상품 수정 기능 추가
hpk5802 Jan 8, 2025
66c7c1b
[refactor]: 로그아웃 시 home으로 이동 처리
hpk5802 Jan 8, 2025
710b677
[fix]: 배포 위해 임시로 update, delete 핸들러 추가
hpk5802 Jan 10, 2025
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
24 changes: 15 additions & 9 deletions components/addItem/ImgFileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,22 @@ function ImgFileInput({
dispatch,
}: PropsWithChildren<ImgFileInputProps>) {
const inputRef = useRef<HTMLInputElement>(null);
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(images[0]);
const [showWarn, setShowWarn] = useState(false); // 이미지를 1개 이상 추가하려고 할 때를 위한 state
const isFilled = images.length > 0;

/**
* form의 이미지를 저장하고 화면에 추가한 이미지를 렌더링
* @param {*} path 인코딩된 파일 스트링
*/
const setImg = (path?: string) => {
dispatch({ type: "SET_IMAGES", payload: path ? [path] : [] });
const setImg = (path?: File) => {
if (path) {
setThumbnailUrl(URL.createObjectURL(path));
dispatch({ type: "SET_IMAGES", payload: [path] });
} else {
setThumbnailUrl(null);
dispatch({ type: "SET_IMAGES", payload: [] });
}
};

/**
Expand All @@ -52,8 +59,7 @@ function ImgFileInput({
const files = e.target.files;
if (files && files[0]) {
const imgUrl = files[0];
const imgPath = URL.createObjectURL(imgUrl);
setImg(imgPath);
setImg(imgUrl);
Comment on lines 61 to +62
Copy link
Collaborator

Choose a reason for hiding this comment

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

imgUrl을 사용하는 곳이 한 군데 뿐이라면 따로 변수를 만들지 않고 setImg(files[0]) 이런식으로 작성하셔도 됩니다.


if (inputRef.current) inputRef.current.value = "";
}
Expand All @@ -68,13 +74,13 @@ function ImgFileInput({
};

/**
* 컴포넌트 언마운트 시 객체 URL이 있으면 해제
* 컴포넌트 언마운트 시 생성한 URL 해제
*/
useEffect(() => {
return () => {
if (images.length > 0) URL.revokeObjectURL(images[0]);
if (thumbnailUrl) URL.revokeObjectURL(thumbnailUrl);
};
}, [images]);
}, [thumbnailUrl]);

return (
<div className='form-input-wrap'>
Expand All @@ -100,9 +106,9 @@ function ImgFileInput({
</span>
이미지 등록
</button>
{isFilled && (
{thumbnailUrl && (
<div className='thumbnail'>
<Image fill src={images[0]} alt='thumbnail' />
<Image fill src={thumbnailUrl} alt='thumbnail' />
<button
type='button'
className='btn-delete-thumbnail'
Expand Down
2 changes: 2 additions & 0 deletions components/common/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ function Header() {
const handleLogout = () => {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("userId");
Comment on lines 22 to +24
Copy link
Collaborator

Choose a reason for hiding this comment

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

localStorage의 키 값들도 상수로 관리해주셔도 좋습니다.

setHasToken(false);
router.push("/");
};

return (
Expand Down
33 changes: 22 additions & 11 deletions components/detail/DetailInquiry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,35 @@ interface DetailInquiryProps {
content: string;
writer: any;
updatedAt: string;
onUpdate: (id: string, updatedContent: string) => void;
onDelete: (id: string) => void;
}

function DetailInquiry({ id, content, writer, updatedAt }: DetailInquiryProps) {
function DetailInquiry({
id,
content,
writer,
updatedAt,
onUpdate,
onDelete,
}: DetailInquiryProps) {
const [isEditing, setIsEditing] = useState(false); // 수정 상태를 할당할 state
const [comment, setComment] = useState(content); // 문의하기 text를 할당할 state
const userId = localStorage.getItem("userId");

/**
* 취소 버튼 클릭 시 comment에 부모에서 받은 content를 할당
*/
const handleCancelEdit = () => {
setIsEditing(false);
setComment(content);
};

/**
* save하면 서버에 request 보내고 response 받아서 commnets 업데이트
*/
const handleSaveEdit = async () => {
try {
const updateData: updateCommentInterface = { content: comment };
const response = await updateComment(id, updateData); // Jwt Token 추가 예정
const data = await response.json(); // 서버에서 받은 response로 새 댓글 추가하는 기능은 추후 추가
console.log(data); // netlify build error 방지 임시 콘솔 추가 - The build failure is due to a linting error
const response = await updateComment(id, updateData);

if (response.ok) {
onUpdate(id, comment);
}
setIsEditing(false);
} catch (error) {
console.log(error);
Expand All @@ -41,7 +47,12 @@ function DetailInquiry({ id, content, writer, updatedAt }: DetailInquiryProps) {

return (
<div className='content-inquiry'>
{!isEditing && <DropDownInquiry setIsEditting={setIsEditing} />}
{!isEditing && String(writer.id) === userId && (
<DropDownInquiry
onEdit={() => setIsEditing(true)}
onDelete={() => onDelete(id)}
/>
)}
<div className='inquiry-comment'>
{isEditing ? (
<textarea
Expand Down
207 changes: 175 additions & 32 deletions components/detail/DetailProduct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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}>
Copy link
Collaborator

Choose a reason for hiding this comment

The 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>
</>
);
}

Expand Down
Loading
Loading