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
Expand Up @@ -8,7 +8,7 @@ export default function App() {
<Routes>
<Route path='/' element={<IndexPage />} />
<Route path='/items' element={<ItemPage />} />
<Route path='/add' element={<AddItem />} />
<Route path='/additem' element={<AddItem />} />
</Routes>
);
}
190 changes: 190 additions & 0 deletions src/pages/add-item/components/AddItemForm.jsx
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
컴포넌트로 분리하신 점은 좋습니다. 다만 지금의 분리의 경우 그냥 add-item 페이지가 다른 컴포넌트로 분리된 것이라 분리의 장점이 없다고 생각합니다. src/pages/items/index.jsx 처럼 해당 페이지 로직들이 src/pages/add-item/index.jsx에 있는 것이 더 구조상 장점이 있을 것 같아요!
분리를 하신다면 페이지 헤더와 인풋들을 각각 분리하시고 src/pages/add-item/index.jsx에서 조합하시는 것을 추천드립니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import Input from './Input';
import { useState, useRef, useEffect } from 'react';
import TagInput from './TagInput';

const AddItemForm = () => {
const [formData, setFormData] = useState({
name: '',
description: '',
price: '',
tag: [],
img: null,
});

const [tagInput, setTagInput] = useState('');
const [productImg, setProductImg] = useState(null);
const [previewImg, setPreviewImg] = useState(null);
const [isSubmitable, setIsSubmitable] = useState(false);
const fileInputRef = useRef(null);

useEffect(() => {
const { name, description, price, tag } = formData;
const allFilled =
name.trim() &&
description.trim() &&
price.trim() &&
Array.isArray(tag) &&
tag.length > 0;
setIsSubmitable(!!allFilled);
}, [formData]);
Comment on lines +17 to +29
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
form이 valid할 경우를 판단하는 isSubmitable state의 경우 formData state로 대체가 가능할 것 같아요.

Suggested change
const [isSubmitable, setIsSubmitable] = useState(false);
const fileInputRef = useRef(null);
useEffect(() => {
const { name, description, price, tag } = formData;
const allFilled =
name.trim() &&
description.trim() &&
price.trim() &&
Array.isArray(tag) &&
tag.length > 0;
setIsSubmitable(!!allFilled);
}, [formData]);
const fileInputRef = useRef(null);
const isFormValid = () => {
const { name, description, price, tag } = formData;
return name.trim() &&
description.trim() &&
price.trim() &&
Array.isArray(tag) &&
tag.length > 0;
}
// 사용시 : <button disabled={!isFormValid()}>등록</button>


const handleImgUpload = (e) => {
const file = e.target.files?.[0];
if (!file) return;

setProductImg(file);

const reader = new FileReader();
reader.onload = () => {
setPreviewImg(reader.result);
};
reader.readAsDataURL(file);

setFormData((prev) => ({
...prev,
img: file,
}));
};

const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};

const handleTagChange = (e) => {
setTagInput(e.target.value);
};

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.

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

https://toby2009.tistory.com/53

if (e.key === 'Enter') {
e.preventDefault();
const value = tagInput.trim();
if (value && !formData.tag.includes(value)) {
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 와 같은 방식으로 피드백을 주시면 더 좋을 것 같아요.

setFormData((prev) => ({
...prev,
tag: [...prev.tag, value],
}));
setTagInput('');
}
}
};

const handleRemoveTag = (tagToRemove) => {
setFormData((prev) => ({
...prev,
tag: prev.tag.filter((tag) => tag !== tagToRemove),
}));
};

const imgOnClick = () => {
fileInputRef.current?.click();
};

return (
<div className='p-4'>
<form>
<div className='flex justify-between items-center mb-4'>
<p className='text-[2rem] font-bold leading-none'>상품 등록하기</p>
<button
type='submit'
Comment on lines +88 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.

👍 칭찬
적절한 form 사용 좋습니다!

disabled={!isSubmitable}
className={`h-[4.4rem] px-[2rem] rounded text-[1.6rem] font-[400] ${
isSubmitable
? 'bg-[#3692ff] text-white cursor-pointer'
: 'bg-[#9ca3af] text-gray-600 cursor-not-allowed'
}`}
>
등록
</button>
</div>

<div className='mb-4 mt-6'>
<p className='mb-2 font-[700] text-[2rem] text-[#1F2937]'>
상품 이미지
</p>
<div className='flex gap-4'>
<div
onClick={imgOnClick}
className='w-[16.8rem] h-[16.8rem] sm:w-[28.2rem] sm:h-[28.2rem] bg-[#f3f4f6] rounded flex flex-col items-center justify-center cursor-pointer'
>
<div className='text-[#9ca3af] text-[2rem] mb-1'>+</div>
<div className='text-[#9ca3af] text-[1.6rem]'>이미지 등록</div>
</div>
Comment on lines +109 to +115
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
label 태그로 작성하셔서 file input과 연결하시는 것이 시멘틱하고 더 좋을 것 같아요!


{previewImg && (
<div className='relative w-[16.8rem] h-[16.8rem] sm:w-[28.2rem] sm:h-[28.2rem]'>
<img
src={previewImg}
alt='preview'
className='w-full h-full object-cover border rounded'
/>
<button
type='button'
onClick={() => {
setPreviewImg(null);
setProductImg(null);
setFormData((prev) => ({ ...prev, img: null }));
if (fileInputRef.current) fileInputRef.current.value = '';
}}
className='absolute top-2 right-2 bg-gray-200 rounded-full w-6 h-6 flex items-center justify-center'
>
<span className='text-gray-600 text-lg'>×</span>
</button>
</div>
)}
</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={fileInputRef}
onChange={handleImgUpload}
style={{ display: 'none' }}
/>
</div>

<div className='flex flex-col gap-10'>
<Input
type='text'
placeholder='상품명을 입력해주세요'
inputName='상품명'
name='name'
value={formData.name}
onChange={handleChange}
Comment on lines +155 to +156
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
지금처럼 상품명을 useState로 제어하면 사용자가 입력할 때마다 컴포넌트가 리렌더링됩니다.
지금 코드상에서는 상품명을 바꿔주는 것이 아니라 제어 컴포넌트로 관리할 필요가 없을 것 같아요!
이를 비제어 컴포넌트로 바꾸셔서 불필요한 리렌더링을 줄이시는 것을 추천드려요!

Suggested change
value={formData.name}
onChange={handleChange}
onChange={handleChange}

/>

<Input
type='textarea'
placeholder='상품소개를 입력해주세요'
inputName='상품 소개'
name='description'
value={formData.description}
onChange={handleChange}
/>

<Input
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.

❗️ 수정요청
이런 경우 number 가 더 적절할 것 같아요!

Suggested change
type='text'
type='number'

placeholder='판매 가격을 입력해주세요'
inputName='판매가격'
name='price'
value={formData.price}
onChange={handleChange}
/>

<TagInput
tagInput={tagInput}
handleTagChange={handleTagChange}
handleTagKeyDown={handleTagKeyDown}
tag={formData.tag}
handleRemoveTag={handleRemoveTag}
/>
</div>
</form>
</div>
);
};

export default AddItemForm;
34 changes: 34 additions & 0 deletions src/pages/add-item/components/Input.jsx
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
input 과 textarea 두개의 컴포넌트로 나누시는 게 더 명확하고 좋을 것 같아요.
지금과 같은 구조에서는 textarea의 경우 필요없는 prop을 받아야하고 이로인해 명확성도 떨어지는 것 같아요.
특히 type의 경우 input에서는 input 태그의 type 속성이고 Input 컴포넌트에서는 'textarea' 이거나 input 태그의 type 속성 이라 더 모호한 것 같아요~

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const Input = ({
type = 'text',
placeholder = '입력',
value,
onChange,
inputName = '상품',
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
label이 더 적절할 것 같아요~
하단의 h2태그도 label 태그로 바꿔주시고 input이랑 연결해주시면 더 좋겠습니다!

name,
}) => {
return (
<div className='flex flex-col mt-8'>
<h2 className='font-[700] text-[2rem] text-[#1F2937]'>{inputName}</h2>
{type === 'textarea' ? (
<textarea
placeholder={placeholder}
value={value}
onChange={onChange}
name={name}
className='w-full px-3 py-4 text-[2rem] resize-none h-[32.4rem]'
/>
) : (
<input
type={type}
placeholder={placeholder}
value={value}
onChange={onChange}
name={name}
className='w-full px-3 py-4 text-[2rem] bg-[#f3f3f6]'
/>
)}
</div>
);
};

export default Input;
42 changes: 42 additions & 0 deletions src/pages/add-item/components/TagInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const TagInput = ({
tagInput,
handleTagChange,
handleTagKeyDown,
tag,
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
타입을 추측할 수 있게 tags, tagList와 같은 이름을 추천드려요!

handleRemoveTag,
}) => {
return (
<div className='mb-4'>
<label className='block font-[700] text-[2rem] text-[#1F2937]'>
태그
</label>
<input
type='text'
placeholder='태그를 입력해주세요'
value={tagInput}
onChange={handleTagChange}
onKeyDown={handleTagKeyDown}
className='w-full px-3 py-4 text-[2rem] bg-[#f3f3f6]'
/>
<div className='flex flex-wrap gap-2 mt-2'>
{tag.map((tag, index) => (
<span
key={index}
className='px-2 py-1 font-[400] text-[#1F2937] rounded-full text-[1.6rem] flex items-center gap-1'
>
#{tag}
<button
type='button'
onClick={() => handleRemoveTag(tag)}
className='text-white text-[1.6rem] ml-1 bg-gray-500 rounded-3xl'
>
&times;
</button>
Comment on lines +29 to +34
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
디자인과 같은 X 버튼이면 좋겠어요!

</span>
))}
</div>
</div>
);
};

export default TagInput;
14 changes: 13 additions & 1 deletion src/pages/add-item/index.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
import NavBar from '../../components/NavBar';
import AddItemForm from './components/AddItemForm';

export default function AddItem() {
return <div>상품추가페이지</div>;
return (
<>
<NavBar />
<div className='bg-white min-h-screen'>
<div className='container mx-auto px-[1.6rem] md:px-[2.4rem] py-6 max-w-[120rem] mt-[2.4rem]'>
<AddItemForm />
</div>
</div>
</>
);
}
2 changes: 1 addition & 1 deletion src/pages/items/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default function ItemPage() {
/>
<button
onClick={() => {
navigate('/add');
navigate('/additem');
}}
className='bg-blue-500 text-white px-[2.3rem] py-[1.2rem] whitespace-nowrap text-2xl rounded-3xl cursor-pointer'
>
Expand Down
Loading