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
2 changes: 1 addition & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Route, Routes } from "react-router";
import AddItem from "./components/Additem/AddItem";
import AddItem from "./components/AddItem/AddItem";
import Nav from "./common/Nav/Nav";
import Items from "./components/Items/Items";
import Products from "./components/Products/Products";
Expand Down
167 changes: 166 additions & 1 deletion src/components/AddItem/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,168 @@
import styles from "./AddItem.module.scss";
import { useState, useRef } from "react";

export default function AddItem() {
return <div>addItem</div>;
const [imagePreview, setimagePreview] = useState(null);
const [formData, setFormData] = useState({
image: "",
name: "",
about: "",
price: "",
tag: [],
});
const inputRef = useRef();
const active = Object.values(formData).every((value) =>
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
boolean 타입임을 알 수 있고 어떤 값인지 알 수 있는 이름을 추천드려요~

Suggested change
const active = Object.values(formData).every((value) =>
const isFormValid = Object.values(formData).every((value) =>

Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
요구사항에서 이미지가 없어도 '등록' 버튼이 활성화되게 하라고 되어 있어요~ 확인 후 요구사항대로 구현하시면 더 좋을 것 같아요!

Array.isArray(value) ? value.length > 0 : value
);

function handleChange(e) {
const { name, value } = e.target;
if (name === "image") {
const newImage = e.target.files[0];
setimagePreview(URL.createObjectURL(newImage));
setFormData((prev) => ({ ...prev, image: newImage }));
} else {
setFormData((prev) => ({ ...prev, [name]: value }));
}
}
function handleTag(e) {
if (e.key === "Enter") {
e.preventDefault(); //폼 내부에서 엔터 -> 자동 제출되는거 막음
setTimeout(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

❓ 질문
어떤 이유로 setTimeout으로 태그를 추가하는 로직으로 짜셨을까요?
제 생각에는 그럴 이유가 없을 것 같아서요.
만약 한글로 태그 입력시 이를 막기 위해서라면 isComposing 속성을 이용하시면 더 좋을 것 같아요!

https://developer.mozilla.org/ko/docs/Web/API/KeyboardEvent/isComposing

const newValue = inputRef.current.value.trim();
if (newValue) {
setFormData((prev) => ({
...prev,
tag: [...prev.tag, newValue],
}));
}
inputRef.current.value = "";
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
값을 조작하기 위해서 ref를 쓰신 거라면 input value에 state를 연결해 제어 컴포넌트 방식으로 하시는 것이 적절한 방식입니다.
React에서는 DOM을 직접 조작하기보다는 상태 기반으로 데이터 흐름을 제어하는 것이 권장되며 ef는 실제 포커스 조작이나 스크롤 이동 등 DOM에 직접 접근해야 할 경우 사용하시면 됩니다~

}, 0);
}
}
const handleSubmit = (e) => {
e.preventDefault();
console.log(formData); // 서버 전송 등 처리
};

console.log(formData);
function handleRemoveTag(tagToRemove) {
setFormData((prev) => ({
...prev,
tag: prev.tag.filter((tag) => tag !== tagToRemove),
}));
}

return (
<>
<form className={styles.form} onSubmit={handleSubmit}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

Comment on lines +57 to +58
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
form 하나만 반환하고 있어서 Fragment로 감쌀 필요가 없을 것 같아요.

Suggested change
<>
<form className={styles.form} onSubmit={handleSubmit}>
<form className={styles.form} onSubmit={handleSubmit}>

<header className={styles["form__header"]}>
<h1 className={styles.h1}>상품 등록하기</h1>
<button
className={`${styles["submit--active"]} ${
active ? "" : styles["submit--disabled"]
}`}
type="submit"
disabled={!active}
>
등록
</button>
</header>
<div className={styles["form__body"]}>
<div className={styles["input__image"]}>
<label
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
지금 이미지 삭제 버튼을 눌렀을 때 formData 값만 변경하고 있어서 제대로 동작하지 않습니다.
동일한 이미지를 등록했다가 지우시고 다시 동일한 이미지를 등록하려고 해보시고 잘 동작하도록 로직을 수정해보시면 좋겠습니다~

htmlFor="input-image"
className={`${styles["input__label"]} ${styles["image--input"]}`}
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
className={`${styles["input__label"]} ${styles["image--input"]}`}
className={`${styles["input__label"]} ${styles["image--input"]}`}

>
상품 이미지
</label>
<input
id="input-image"
type="file"
accept="image/png, image/gif, image/jpeg"
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

name="image"
onChange={handleChange}
placeholder="이미지 등록"
style={{ display: "none" }}
/>
{formData.image && (
<div className={styles["image--preview"]}>
<img src={imagePreview} alt="미리보기" />
<button
className={`${styles.xButton} ${styles["delete-image"]}`}
Comment on lines +91 to +92
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
button type 속성의 기본값은 submit입니다~ type을 button으로 명시해주세요!

Suggested change
<button
className={`${styles.xButton} ${styles["delete-image"]}`}
<button
type="button"
className={`${styles.xButton} ${styles["delete-image"]}`}

onClick={() =>
setFormData((prev) => ({ ...prev, image: "" }))
}
/>
</div>
)}
</div>
{formData.image && (
<p className={styles.warning}>
*이미지 등록은 최대 1개까지 가능합니다.
</p>
)}
<label htmlFor="input-name" className={styles["input__label"]}>
상품명
</label>
<input
id="input-name"
className={styles["input__short-text"]}
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="상품명을 입력해주세요"
/>
<label htmlFor="input-about" className={styles["input__label"]}>
상품 소개
</label>
<textarea
id="input-about"
className={styles["input__long-text"]}
type="text"
name="about"
value={formData.about}
onChange={handleChange}
placeholder="상품 소개를 입력해주세요"
/>
<label htmlFor="input-price" className={styles["input__label"]}>
판매가격
</label>
<input
id="input-price"
className={styles["input__short-text"]}
type="text"
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
지금과 같은 경우 input type number가 더 적절할 것 같습니다!

name="price"
value={formData.price}
onChange={handleChange}
placeholder="판맥 가격을 입력해주세요"
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
placeholder="판맥 가격을 입력해주세요"
placeholder="판매 가격을 입력해주세요"

/>
<label htmlFor="input-tag" className={styles["input__label"]}>
태그
</label>
<input
id="input-tag"
className={styles["input__short-text"]}
type="text"
name="tag"
ref={inputRef}
onKeyDown={handleTag}
placeholder="태그를 입력해주세요"
/>
<ul className={styles.tags}>
{formData.tag.map((tag, index) => (
<li key={index} className={styles.tag}>
<span>#{tag}</span>
<button
className={`${styles.xButton} ${styles["delete-tag"]}`}
onClick={() => handleRemoveTag(tag)}
/>
</li>
))}
</ul>
</div>
</form>
</>
);
}
180 changes: 180 additions & 0 deletions src/components/AddItem/AddItem.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
@import "../../styles/variables.scss";
.form {
width: 1200px;
margin: 0 auto;
}
.form__header {
display: flex;
justify-content: space-between;
align-items: center;
margin: 24px 0;
}
.h1 {
@include font-20-bold;
}
.submit--active {
@include font-16-semiBold;
text-align: center;
width: 74px;
height: 42px;
border-radius: 8px;
color: #f3f4f6;
background-color: #3692ff;
}
.submit--disabled {
background-color: #9ca3af;
}

.form__body {
display: flex;
flex-direction: column;
font-size: 20px;
}

.input__label {
@include font-18-bold;
margin: 32px 0 16px 0;
}

.input__short-text,
.input__long-text {
@include font-16-regular;
flex-basis: 100%;
min-height: 56px;
background-color: #f3f4f6;
color: #1f2937;
padding-left: 24px;
border-radius: 12px;
}
.input__long-text {
min-height: 282px;
padding-top: 16px;
}

.input__image {
position: relative;
margin-bottom: 298px;
}

.image--input::after {
@include font-16-regular;
content: "이미지 등록";
position: absolute;
top: 200px;
left: 104px;
color: #9ca3af;
}
.image--input::before {
content: "";
width: 282px;
height: 282px;
position: absolute;
top: 42px;
left: 0px;
border-radius: 12px;
background-color: #f3f4f6;
background-image: url("../../image/plus.svg");
background-repeat: no-repeat;
background-position-x: 117px;
background-position-y: 98px;
}
.image--preview {
position: absolute;
top: 42px;
left: 306px;
border-radius: 12px;
background-color: #f3f4f6;
}
.image--preview img {
width: 282px;
height: 282px;
border-radius: 12px;
-o-object-fit: cover;
object-fit: cover;
}

.xButton {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #9ca3af;
background-image: url("../../image/x.svg");
background-repeat: no-repeat;
background-position: center;
}
.delete-image {
position: absolute;
top: 14px;
left: 249px;
}
.warning {
@include font-16-regular;
margin-top: 16px;
color: #f74747;
}
.tags {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
width: 100%;
gap: 12px;
margin: 14px 0 69px 0;
}
.tag-wrapper {
position: relative;
}
.tag {
@include font-16-regular;
padding: 5px 12px;
border-radius: 26px;
color: #1f2937;
background-color: #f3f4f6;
display: flex;
gap: 10px;
align-items: center;
}

@media (max-width: 744px) {
.form {
width: 696px;
}

.image--input::after {
top: 138px;
left: 47px;
color: #9ca3af;
}
.image--input::before {
width: 168px;
height: 168px;
background-position-x: 60px;
background-position-y: 41px;
}

.input__image {
margin-bottom: 184px;
}
.input__label {
margin-top: 24px;
}
.image--preview {
top: 37px;
left: 178px;
}
.image--preview img {
width: 168px;
height: 168px;
}
.delete-image {
top: 14px;
left: 135px;
}
.warning {
margin-top: 8px;
}
}
@media (max-width: 376px) {
.form {
width: 346px;
}
}
Loading