Skip to content
Binary file added src/assets/deleteIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/plusIcon.png
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/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const Header = () => {
<Link to={"/FreeBoard"} css={aTag}>자유게시판</Link>
<Link to={"/Items"}
css={[aTag,
location.pathname ==='/Items'&& css`
(location.pathname ==='/Items' || location.pathname === '/additems') && css`
color: var(--blue)`]}
>
중고마켓
Expand Down
171 changes: 170 additions & 1 deletion src/components/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,6 +1,175 @@
import {
itemWrapper,
addItem,
contentHeader,
headerButton,
addItemBox,
addItemImageWrapper,
plusIconStyle,
imgRowWrapper,
previewBox,
previewImg,
deleteImage,
alertMessage,
itemHashTagWrapper,
ItemTag,
hashTagContainer,
} from './AddItemStyle'
import plusIcon from '../../assets/plusIcon.png'
import deleteIcon from '../../assets/deleteIcon.png'
import { useRef, useState } from 'react';

/** @jsxImportSource @emotion/react */

const AddItem = () => {
return <div>AddItem</div>;

const [imgPreviewUrl, setImgPreviewUrl] = useState(null); // 추가했을때 이미지프리뷰
const [showWarning, setShowWarning] = useState(false);
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
boolean 타입을 추론할 수 있게 isWarning, isImageUploaded 같은 이름을 추천드립니다~
추가로 imgPreviewUrl state를 통해 업로드된 이미지가 존재하는지 파악할 수 있어 해당 state는 없어도 될 것 같아요~

const [itemName, setItemName] = useState("");
const [itemDescription, setItemDiscription] = useState("");
const [itemPrice, setItemPrice] = useState("");
const [itemTag, setItemTag] = useState("");
const [itemHashTag, setItemHashTag] = useState([]);

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

if (imgPreviewUrl !== null) {
setShowWarning(true)
return
}

if (file) {
const preview = URL.createObjectURL(file);
setImgPreviewUrl(preview)
}
}

const handleFileDelete = () => {
setImgPreviewUrl(null);
setShowWarning(false);
}

const handleSubmitButton = () => {

}
Comment on lines +53 to +55
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
const handleSubmitButton = () => {
}


const handleDeleteTag = (tagToDelete) => {
setItemHashTag(itemHashTag.filter(tag => tag !==tagToDelete));
}

const tagEndRef = useRef(null); // 태그 생성시 스크롤 이동을 위함

return (
<>
<form css={addItem}>
<div css={contentHeader}>
<h3>상품 등록하기</h3>
<button
type="submit"
css={headerButton}
onClick={handleSubmitButton}
disabled={
itemName.trim() === "" ||
itemDescription.trim() === "" ||
itemPrice.trim() === "" ||
itemHashTag.length === 0 ||
!imgPreviewUrl
}
>
등록
</button>
</div>
<div css={itemWrapper}>
<div >
<p>상품 이미지</p>
<div css={imgRowWrapper}>
<label htmlFor='fileUpload' css={addItemImageWrapper}>
<div css={addItemBox}>
<img src={plusIcon} alt='추가' css={plusIconStyle} />
<span>이미지 등록</span>
</div>
</label>
{imgPreviewUrl && (
<div css={previewBox}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
해당 영역이 header위로 올라가네요. 확인해보시고 수정해보세요~
스크린샷 2025-05-14 오후 4 51 24

<img src={imgPreviewUrl} alt='선택한 사진' css={previewImg} />
<button onClick={handleFileDelete}>
<img src={deleteIcon} alt='삭제버튼' css={deleteImage} />
</button>
</div>
)}
</div>
<input
id='fileUpload'
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

style={{display: 'none'}}
onChange={handleFileChange}
/>
</div>

{showWarning && (
<p css={alertMessage}>이미지는 한 장만 선택할 수 있습니다.</p>
)}

<div>
<label htmlFor='name'>상품명</label>
<input id="name" placeholder="상품명을 입력해주세요" value={itemName} onChange={(e)=> setItemName(e.target.value)}></input>
</div>

<div>
<label>상품 소개</label>
<textarea id="description" placeholder="상품 소개를 입력해주세요" value={itemDescription} onChange={(e) => setItemDiscription(e.target.value)}></textarea>
</div>

<div>
<label>판매가격</label>
<input id="price" placeholder="판매가격을 입력해주세요" type='number' value={itemPrice} onChange={(e) => setItemPrice(e.target.value)}></input>
Comment on lines +126 to +127
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
<label>판매가격</label>
<input id="price" placeholder="판매가격을 입력해주세요" type='number' value={itemPrice} onChange={(e) => setItemPrice(e.target.value)}></input>
<label htmlFor="price">판매가격</label>
<input id="price" placeholder="판매가격을 입력해주세요" type='number' value={itemPrice} onChange={(e) => setItemPrice(e.target.value)}></input>

</div>

<div>
<label>태그</label>
<input
id="tag"
placeholder="태그를 입력해주세요"
value={itemTag} onChange={(e) => setItemTag(e.target.value)}
onKeyDown={(e) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
onKeyDown 내부 로직이 길어지면 컴포넌트가 복잡해지므로, 별도의 핸들러 함수로 분리하면시면 좋겠습니다.

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

if (e.key === "Enter"&& itemTag.trim() !== "") {
e.preventDefault();

const newTag = itemTag.trim();
if (!itemHashTag.includes(newTag)) {
setItemHashTag((prev) => [...prev, newTag])
}
Comment on lines +141 to +143
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 와 같은 방식으로 피드백을 주시면 더 좋을 것 같아요.

setItemTag("")
//태그 생성시 스크롤 이동
setTimeout(() => {
tagEndRef.current?.scrollIntoView({behavior: "smooth"})
},0)
}
}}
></input>
</div>

{itemHashTag.length > 0 && (
<div css={hashTagContainer}>
{itemHashTag.map((tag) => (
<div key={tag} css={itemHashTagWrapper}>
<span css={ItemTag}>
#{tag}
</span>
<button type='button' onClick={() => handleDeleteTag(tag)}>
<img src={deleteIcon} alt='삭제버튼' css={deleteImage} />
</button>
</div>
))}
<div ref={tagEndRef} />
</div>
)}
</div>
</form>
</>
);
};

export default AddItem;
201 changes: 201 additions & 0 deletions src/components/pages/AddItemStyle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { css } from '@emotion/react';


const itemWrapper = css`
color: var(--gray-800);
font-weight: 700;
> div {
margin-top: 32px;
}
> div > input, textarea {
border-radius: 12px;
width: 100%;
height: 56px;
background: var(--gray-100);
margin-top: 16px;
padding-left: 20px;
border: none;
cursor: pointer;
> span {
color: var(--gray-400);
font-weight: 400;
}
}
> div > textarea {
height: 282px;
padding-top: 15px;
}
// wrapper에 div 안 input, textarea, span 에 적용
> div > input::placeholder, textarea::placeholder, span {
color: var(--gray-400);
font-weight: 400;
}
`

const addItem = css`
max-width: 1200px;
margin: 0 auto;
padding: 0 16px;
`
const contentHeader = css`
width: auto;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 24px;
> button {
border-radius: 8px;
border: none;
width: 74px;
height: 42px;
background: var(--gray-400);
color: white;
}
`

const headerButton = css`
cursor: pointer;
&:not(:disabled) {
background: var(--blue);
}
&:disabled {
cursor: not-allowed;
}
`

const addItemImageWrapper = css`
`
Comment on lines +74 to +76
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
빈 css block은 지우시는 것을 추천드려요!

Suggested change
const addItemImageWrapper = css`
`


const addItemBox = css`
margin-top: 16px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px;
width: 282px;
aspect-ratio: 1/1;
background: var(--gray-100);
border-radius: 12px;
cursor: pointer;
@media (max-width: 768px) {
width: 168px;
}
`

const plusIconStyle = css`
width: 48px;
aspect-ratio: 1/1;
`

const imgRowWrapper = css`
display: flex;
gap: 24px;
@media (max-width: 768px) {
gap: 10px;
}
`

const previewBox = css`
position: relative;
margin-top: 16px;
display: flex;
width: 282px;
aspect-ratio: 1/1;
@media (max-width: 768px) {
width: 168px;
}
> button {
position: absolute;
background: transparent;
border: none;
cursor: pointer;
top: 8px;
right: 8px;
transition: all 0.2s ease;
&:hover {
transform: scale(1.1);
}
}
`

const previewImg = css`
width: 100%;
aspect-ratio: 1/1;
object-fit: cover;
border-radius: 12px;
`

const deleteImage = css`
width: 22px;
height: 24px;
`

const alertMessage = css`
color: rgba(247, 71, 71, 1);
font-weight: 400;
font-size: 16px;
`

const itemHashTagWrapper = css`
background: var(--gray-100);
border-radius: 26px;
display: inline-flex;
padding: 5px 12px 5px 16px;;
> span {
color: var(--gray-800);
display: flex;
align-items: center;
}
> button {
background: transparent;
border: none;
cursor: pointer;
padding-top: 2px;
}
`

const ItemTag = css`
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️수정요청
스타일을 정의할 때 ItemTag처럼 대문자로 시작하면 컴포넌트처럼 보일 수 있어 혼동을 줄 수 있습니다.
스타일 객체는 일반 변수로 간주되므로 첫문자를 소문자로 작성하는 것이 좋을 것 같아요!

color: var(--gray-800);
`

const hashTagContainer = css`
margin-top: 14px;
display: flex;
flex-wrap: wrap;
gap: 12px;
`

export {
itemWrapper,
addItem,
contentHeader,
headerButton,
addItemBox,
addItemImageWrapper,
plusIconStyle,
imgRowWrapper,
previewBox,
previewImg,
deleteImage,
alertMessage,
itemHashTagWrapper,
ItemTag,
hashTagContainer,
};
Loading