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
5 changes: 5 additions & 0 deletions public/ic_X.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function App() {
<Route path="/privacy" element={<Privacy />} />
<Route path="/FAQ" element={<FAQ />} />
<Route path="/community" element={<Community />} />
<Route path="/additem" element={<AddItem />} />
<Route path="/item/additem" element={<AddItem />} />
</Routes>
</BrowserRouter>
);
Expand Down
107 changes: 107 additions & 0 deletions src/components/AddItem/AddItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useState } from "react";

import ProductImg from "./components/ProductImg";
import styles from "./styles/AddItem.module.css";

export default function AddItemContent() {
const [productName, setProductName] = useState("");
const [description, setDescription] = useState("");
const [price, setPrice] = useState("");
const [tagInput, setTagInput] = useState("");
const [tags, setTags] = useState([]);
const [imagePreview, setImagePreview] = useState(null);

const isFormValid =
productName && description && price && tags.length > 0 && imagePreview;

const handleKeyDown = (e) => {
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 와 같은 방식으로 피드백을 주시면 더 좋을 것 같아요.

if (e.key === "Enter") {
e.preventDefault();
const trimmed = tagInput.trim();
if (trimmed && !tags.includes(trimmed)) {
setTags((prev) => [...prev, trimmed]);
setTagInput("");
}
}
};

const handleDelete = (targetTag) => {
setTags((prevTags) => prevTags.filter((tag) => tag !== targetTag));
};

return (
<main className={styles.container}>
<section className={styles.addContainer}>
<h1 className={styles.addTitle}>상품 등록하기</h1>
<button disabled={!isFormValid} className={styles.addBtn} type="submit">
등록
</button>
Comment on lines +33 to +38
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
기존 작업하셨던 로그인, 회원가입 페이지처럼 form 요소를 사용하시면 UX측면에서도 좋고, form 단위로 조작하기도 좋습니다~

  • form으로 input과 button을 포함하게 되면, 해당 form의 상호작용가능한 자식요소에서 onsubmit 이벤트가
    발생시 form이 이를 감지할 수 있고, 유저입장에서 enter 키를 눌렀을 때 onsubmit 이벤트가 제출되므로 UX 적으로도 좋습니다.
// case1: form에 onsubmit 이벤트 명시
<form class="login-form" onsubmit="login">
  <input /> // input에서 enter 키 입력시 onsubmit 이벤트 실행
  <button type="submit">login</button> // 해당 버튼 클릭 시 onsubmit 이벤트 실행
</form>

// case2: submit 버튼에 onclick 이벤트 명시
<form class="login-form">
  <input /> // input에서 enter 키 입력시 button의 onclick 이벤트 실행
  <button type="submit" onClick="login">login</button>
</form>

</section>

<section className={styles.contentContainer}>
<h2 className={styles.title}>상품 이미지</h2>
<ProductImg preview={imagePreview} setPreview={setImagePreview} />
</section>

<section className={styles.contentContainer}>
<h2 className={styles.title}>상품명</h2>
<input
className={styles.input}
placeholder="상품명을 입력해주세요"
value={productName}
onChange={(e) => setProductName(e.target.value)}
></input>
</section>

<section className={styles.contentContainer}>
<h2 className={styles.title}>상품 소개</h2>
<textarea
className={styles.inputDescription}
placeholder="상품 소개를 입력해주세요"
value={description}
onChange={(e) => setDescription(e.target.value)}
></textarea>
Comment on lines +57 to +63
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
label 태그로 하셔서 input과 연결하시면 더 좋을 것 같아요.

Suggested change
<h2 className={styles.title}>상품 소개</h2>
<textarea
className={styles.inputDescription}
placeholder="상품 소개를 입력해주세요"
value={description}
onChange={(e) => setDescription(e.target.value)}
></textarea>
<label htmlFor="description" className={styles.title}>상품 소개</label>
<textarea
id="description"
className={styles.inputDescription}
placeholder="상품 소개를 입력해주세요"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>

</section>

<section className={styles.contentContainer}>
<h2 className={styles.title}>판매가격</h2>
<input
className={styles.input}
placeholder="판매 가격을 입력해주세요"
value={price}
onChange={(e) => setPrice(e.target.value)}
></input>
</section>

<section className={styles.contentContainer}>
<h2 className={styles.title}>태그</h2>
<input
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
한글로된 태그를 생성할 때 두번 입력이 발생하는 것 같은 현상이 있습니다. 이는 자모음으로 이루어진 한글을 입력중에 발생하는 현상입니다~
배포사이트에서 한글로 태그를 생성해보시고, 위의 동작을 고쳐보세요~

https://toby2009.tistory.com/53

className={styles.input}
placeholder="태그를 입력해주세요"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleKeyDown}
></input>

<div className={styles.tagList}>
{tags.map((tag, idx) => (
<span key={idx} className={styles.tag}>
#{tag}
<button
className={styles.deleteBtn}
type="button"
onClick={() => handleDelete(tag)}
>
<img
className={styles.deleteImg}
src="/ic_X.svg"
alt="등록 이미지 삭제 아이콘"
/>
</button>
</span>
))}
</div>
</section>
</main>
);
}
54 changes: 54 additions & 0 deletions src/components/AddItem/components/ProductImg.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useRef, useState } from "react";
import styles from "./styles/ProductImg.module.css";

export default function ProductImg({ preview, setPreview }) {
const inputRef = useRef(null);
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
ref가 사용되지 않고 있어서 없어도 될 것 같아요~ 추후 필요하실 때 사용하시는 것을 추천드리겠습니다.


const handleImageChange = (e) => {
const file = e.target.files?.[0];

if (preview) {
alert("이미지는 한 개만 업로드할 수 있습니다.");
return;
}

if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result);
};
reader.readAsDataURL(file);
}
};

const handleDelete = () => {
setPreview(null);
};

return (
<div className={styles.wrapper}>
<label className={styles.uploadBox}>
<div className={styles.textContainer}>
<span className={styles.plus}>+</span>
<p className={styles.text}>이미지 등록</p>
</div>
<input
type="file"
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

ref={inputRef}
onChange={handleImageChange}
style={{ display: "none" }}
/>
</label>

{preview && (
<div className={styles.imagePreviewBox}>
<button className={styles.deleteButton} onClick={handleDelete}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
버튼 타입을 적지 않으시면 reset을 기본값으로 가지게 됩니다~ 늘 명시해주시면 좋습니다.

Suggested change
<button className={styles.deleteButton} onClick={handleDelete}>
<button type="button" className={styles.deleteButton} onClick={handleDelete}>

<img src="/ic_X.svg" alt="등록 이미지 삭제 아이콘" />
</button>
<img src={preview} alt="미리보기" className={styles.previewImg} />
</div>
)}
</div>
);
}
77 changes: 77 additions & 0 deletions src/components/AddItem/components/styles/ProductImg.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
.wrapper {
display: flex;
gap: 2.4rem;
}

.uploadBox {
width: 28.2rem;
height: 28.2rem;
background-color: var(--gray100);
border-radius: 1.2rem;

display: flex;
flex-direction: column;
align-items: center;
justify-content: center;

cursor: pointer;
overflow: hidden;

flex-shrink: 0;
}

.textContainer {
display: inline-flex;
flex-direction: column;
align-items: center;
}

.plus {
font-size: 5rem;
font-weight: 100;
color: var(--gray400);
}

.text {
font-size: 1rem;
color: var(--gray400);
font-weight: 400;
}

.imagePreviewBox {
width: 28.2rem;
height: 28.2rem;
border-radius: 1.2rem;
overflow: hidden;
position: relative;

flex-shrink: 0;
}

.previewImg {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}

.deleteButton {
position: absolute;
top: 8px;
right: 15px;

width: 20px;
height: 20px;
}

@media (max-width: 768px) {
.uploadBox {
width: 16.8rem;
height: 16.8rem;
}

.imagePreviewBox {
width: 16.8rem;
height: 16.8rem;
}
}
Loading
Loading