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
Binary file added src/assets/images/add-Image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
2 changes: 1 addition & 1 deletion src/components/AllProductList.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import HeartIcon from "../assets/icons/icon_heart";
import noImage from "../assets/no-image.png";
import noImage from "../assets/images/no-image.png";

export default function AllProductList({ data = [] }) {
if (!data.length) return null;
Expand Down
11 changes: 11 additions & 0 deletions src/components/Input.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function Input({ id, type = "text", placeholder, ...props }) {
return (
<input
id={id}
type={type}
placeholder={placeholder}
className="w-full p-4 bg-[#F3F4F6] placeholder:text-[#9CA3AF] rounded-md"
{...props}
/>
);
}
Comment on lines +1 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
깔끔한 분리 좋습니다! 다만 추후 classname을 받을 수 있게 하시면 더 좋을 것 같아요.
이렇게 classname을 외부에서 받아야 기본 인풋위에 필요한 스타일을 적용시키며 확장성 있게 사용할 수 있습니다.

Suggested change
export default function Input({ id, type = "text", placeholder, ...props }) {
return (
<input
id={id}
type={type}
placeholder={placeholder}
className="w-full p-4 bg-[#F3F4F6] placeholder:text-[#9CA3AF] rounded-md"
{...props}
/>
);
}
export default function Input({ id, type = "text", placeholder, classname ="", ...props }) {
return (
<input
id={id}
type={type}
placeholder={placeholder}
className={`w-full p-4 bg-[#F3F4F6] placeholder:text-[#9CA3AF] rounded-md ${classname}`}
{...props}
/>
);
}

참고로 tailwind의 경우 작성한 클래스 네임의 순서가 뒤에 있다고 적용이 보장되지 않기 때문에
아래 문서를 읽어보시고 이러한 특징도 고려해서 컴포넌트를 수정해보시면 더 좋겠습니다!

https://yoonho-devlog.tistory.com/199

6 changes: 3 additions & 3 deletions src/components/Nav.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logo from "../assets/panda-logo.png";
import brandName from "../assets/판다마켓.png";
import avatar from "../assets/avatar.png";
import logo from "../assets/images/panda-logo.png";
import brandName from "../assets/images/판다마켓.png";
import avatar from "../assets/images/avatar.png";
import { Link, useLocation } from "react-router-dom";

export default function Nav() {
Expand Down
244 changes: 243 additions & 1 deletion src/pages/AddItem.jsx
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
하나의 파일이 너무 큰 것 같아요~
내부적으로 아이템 생성을 위한 로직들과 state가 custom hook으로 분리되고 UI 들도 컴포넌트로 분리되면,
가독성 측면에서 더 좋을 것 같아요.
컴포넌트로 분리하는 것은 생각의 단위를 나누는 것과 같이 때문에 너무 크게 가지고 가시면 로직을 파악하기도 어렵고 유지보수시에도 불리합니다!

Original file line number Diff line number Diff line change
@@ -1,3 +1,245 @@
import { useState } from "react";
import addImg from "../assets/images/add-Image.png";
import Input from "../components/Input";

export default function AddItem() {
return <h1>add item</h1>;
const [selectedImage, setSelectedImage] = useState(null);
const [showWarning, setShowWarning] = useState(false);
const [productName, setProductName] = useState("");
const [productDescription, setProductDescription] = useState("");
const [productPrice, setProductPrice] = useState("");
const [tagInput, setTagInput] = useState("");
const [tags, setTags] = useState([]);

const isFormValid =
selectedImage &&
productName.trim() &&
productDescription.trim() &&
productPrice.trim();

const handleImageClick = (event) => {
if (selectedImage) {
event.preventDefault();
setShowWarning(true);
} else {
setShowWarning(false);
}
};

const onImageChange = (event) => {
if (selectedImage) {
setShowWarning(true);
return;
}

const {
target: { files },
} = event;
Comment on lines +29 to +37
Copy link
Collaborator

Choose a reason for hiding this comment

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

💬 여담
event 인자를 사용하는 부분이 없어서 처음부터 구조분해할당해서 받아도 될 것 같아요~

Suggested change
const onImageChange = (event) => {
if (selectedImage) {
setShowWarning(true);
return;
}
const {
target: { files },
} = event;
const onImageChange = ({ target }) => {
if (selectedImage) {
setShowWarning(true);
return;
}
const file = target.files?.[0];


if (files && files[0]) {
const file = files[0];
const url = URL.createObjectURL(file);
setSelectedImage(url);
setShowWarning(false);
}
};

const removeSelectedImage = () => {
setSelectedImage(null);
setShowWarning(false);
};

const handleTagKeyDown = (e) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

💬 여담
한글로 태그 입력시 마지막 글자가 중복으로 입력되는 것을 막기위해서 state를 통해 로직을 구성하신 것 같아요~
이럴때 키보드 이벤트의 isComposing 속성을 이용해서 로직을 짜실 수도 있습니다.

if (e.key === "Enter" && tagInput.trim()) {
e.preventDefault();
if (!tags.includes(tagInput.trim())) {
setTags([...tags, tagInput.trim()]);
}
setTagInput("");
}
Comment on lines +53 to +59
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
중복되는 태그가 생성되지 않도록 해주신 점 좋습니다!
다만 이렇게 되면 사용자가 해당 동작에 대한 피드백을 받지 못하므로 왜 태그가 추가되지 않고 인풋값은 초기화되는지에 대해 알 수가 없습니다. 이럴때 alert, toast, input error 와 같은 방식으로 피드백을 주시면 더 좋을 것 같아요.

};

const removeTag = (tagToRemove) => {
setTags(tags.filter((tag) => tag !== tagToRemove));
};

return (
<div className="max-w-[120rem] mx-auto p-7">
{/* <p>상태 확인: {isFormValid ? "✅ 유효함" : "❌ 유효하지 않음"}</p> */}
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
개발중 확인을 위한 코드는 지우고 올려주세요~

Suggested change
{/* <p>상태 확인: {isFormValid ? "✅ 유효함" : "❌ 유효하지 않음"}</p> */}

<form
onSubmit={(e) => e.preventDefault()}
Comment on lines +69 to +70
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="flex flex-col gap-8"
>
<div className="flex justify-between items-center mb-8">
<h1 className="text-[1.6rem] font-[700] text-[#1F2937]">
상품 등록하기
</h1>
<button
type="submit"
disabled={!isFormValid}
className="py-2.5 px-4 rounded-md transition-colors text-white cursor-pointer
bg-[#3692FF] disabled:bg-gray-400 disabled:cursor-not-allowed"
>
등록
</button>
</div>

<h3 className="text-[1.4rem] font-semibold mb-4 text-gray-700">
상품 이미지
</h3>
<div className="flex items-start gap-6">
<label htmlFor="photo" className="flex">
<img
className="w-full h-full max-w-[25rem] max-h-[25rem]"
onClick={handleImageClick}
src={addImg}
role="button"
aria-disabled={!!selectedImage}
/>
</label>
<input
onChange={onImageChange}
type="file"
id="photo"
name="photo"
accept="image/*"
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
input의 accept 속성은 유저가 어떤 파일을 올려야하는지에 대한 힌트를 제공하는 속성입니다.
유저는 파일 업로드시 accept의 명시된 확장자 이외의 파일도 올릴 수 있으므로
실제 upload 함수에서 한번더 확장자를 검사해주시는 것이 좋습니다.

(사용자가 업로드창에서 옵션을 열어 확장자를 바꾸면 아래처럼 보입니다)
스크린샷 2025-05-08 오후 5 53 17

https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/accept

className="hidden"
/>

{selectedImage && (
<div className="relative w-full h-full max-w-[25rem] max-h-[25rem]">
<img
src={selectedImage}
alt="선택된 상품 이미지"
className="w-full h-full object-cover rounded-lg max-w-[25rem] max-h-[25rem]"
/>
<button
type="button"
onClick={removeSelectedImage}
className="absolute top-2 right-2 bg-black bg-opacity-50 text-white rounded-full p-1.5 hover:bg-opacity-75 transition-opacity cursor-pointer"
aria-label="이미지 삭제"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
)}
</div>
{(showWarning || (selectedImage && !showWarning)) && (
<p className="text-orange-600 text-sm mt-4">
* 이미지 등록은 최대 1개까지 가능합니다.
</p>
)}

<div>
<label
htmlFor="productName"
className="block text-[1.4rem] font-[700] text-gray-700 mb-4"
>
상품명
</label>
<Input
id="productName"
placeholder="상품명을 입력해주세요"
value={productName}
onChange={(e) => setProductName(e.target.value)}
/>
</div>
<div>
<label
htmlFor="productDescription"
className="block text-[1.4rem] font-[700] text-gray-700 mb-1"
>
상품 소개
</label>
<textarea
id="productDescription"
placeholder="상품 소개를 입력해주세요"
rows="10"
className="w-full p-4 bg-[#F3F4F6] placeholder:text-[#9CA3AF]"
value={productDescription}
onChange={(e) => setProductDescription(e.target.value)}
/>
</div>
<div>
<label
htmlFor="productPrice"
className="block text-[1.4rem] font-[700] text-gray-700 mb-1"
>
판매가격
</label>
<Input
id="productPrice"
type="number"
placeholder="판매 가격을 입력해주세요"
value={productPrice}
onChange={(e) => setProductPrice(e.target.value)}
/>
</div>
<div>
<label
htmlFor="productTags"
className="block text-[1.4rem] font-[700] text-gray-700 mb-1"
>
태그
</label>
<Input
id="productTags"
placeholder="태그를 입력해주세요"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleTagKeyDown}
/>
<div className="flex gap-2 flex-wrap mt-2">
{tags.map((tag) => (
<div
key={tag}
className="bg-gray-100 text-black px-3 py-2 rounded-full flex items-center text-[1.2rem]"
>
#{tag}
<div
type="button"
onClick={() => removeTag(tag)}
className="ml-2 bg-[#9CA3AF] p-1 rounded-full"
>
Comment on lines +212 to +216
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
이런 클릭 가능한 요소의 경우 버튼을 사용해주세요. 더 적절한 태그가 있는데 div 태그에 onClick 을 주게되면 button에게 기대하는 동작을 제대로 수행하지 못합니다!

Suggested change
<div
type="button"
onClick={() => removeTag(tag)}
className="ml-2 bg-[#9CA3AF] p-1 rounded-full"
>
<button
type="button"
onClick={() => removeTag(tag)}
className="ml-2 bg-[#9CA3AF] p-1 rounded-full"
>

<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.08032 1L9.08032 9"
stroke="#F9FAFB"
stroke-width="1.8"
stroke-linecap="round"
/>
<path
d="M9 1L1 9"
stroke="#F9FAFB"
stroke-width="1.8"
stroke-linecap="round"
/>
</svg>
</div>
</div>
))}
</div>
</div>
</form>
</div>
);
}
Loading