-
Notifications
You must be signed in to change notification settings - Fork 39
[김수민] Sprint6 #183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "React-\uAE40\uC218\uBBFC-sprint6"
[김수민] Sprint6 #183
Changes from all commits
713598a
b80140d
9ef0b73
862274d
075b0c4
202ae58
59b62d9
7cac079
a930066
7963cc7
5b2c8c5
abf5741
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,31 @@ | ||||||||||||||||
| import { createContext, useContext, useState } from 'react'; | ||||||||||||||||
|
|
||||||||||||||||
| export const AddItemFormContext = createContext(); | ||||||||||||||||
|
|
||||||||||||||||
| export function AddItemFormProvider({ children }) { | ||||||||||||||||
| const [name, setName] = useState(''); | ||||||||||||||||
| const [description, setDescription] = useState(''); | ||||||||||||||||
| const [price, setPrice] = useState(''); | ||||||||||||||||
| const [tags, setTags] = useState([]); | ||||||||||||||||
|
|
||||||||||||||||
| const isFormValid = name && description && price && tags.length > 0; | ||||||||||||||||
|
|
||||||||||||||||
| return ( | ||||||||||||||||
| <AddItemFormContext.Provider | ||||||||||||||||
| value={{ | ||||||||||||||||
| name, | ||||||||||||||||
| setName, | ||||||||||||||||
| description, | ||||||||||||||||
| setDescription, | ||||||||||||||||
| price, | ||||||||||||||||
| setPrice, | ||||||||||||||||
| tags, | ||||||||||||||||
| setTags, | ||||||||||||||||
| isFormValid, | ||||||||||||||||
| }}> | ||||||||||||||||
| {children} | ||||||||||||||||
| </AddItemFormContext.Provider> | ||||||||||||||||
| ); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| export const useAddItemForm = () => useContext(AddItemFormContext); | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💊 제안
Suggested change
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import React, { useState } from 'react'; | ||
| import styles from '../styles/AddItemImage.module.css'; | ||
|
|
||
| export default function AddItemImage() { | ||
| const [fileImg, setFileImg] = useState(null); | ||
| const [showFileLimitAlert , setShowFileLimitAlert ] = useState(false); | ||
| const handleFileChange = (e) => { | ||
| if (fileImg) { | ||
| setShowFileLimitAlert (true); | ||
| e.target.value = ''; | ||
| return; | ||
| } | ||
| setFileImg(e.target.files[0]); | ||
| setShowFileLimitAlert (false); | ||
| } | ||
| const handleRemoveFile = () => { | ||
| setFileImg(null); | ||
| setShowFileLimitAlert(false); | ||
| }; | ||
| return ( | ||
| <> | ||
| <div className={styles.addItemImage}> | ||
| <input type='file' name='file' id='file' onChange={handleFileChange} /> | ||
| <label htmlFor='file'> | ||
| <img src='/images/common/ic_plus.svg' alt='이미지 등록' /> | ||
| 이미지 등록 | ||
| </label> | ||
| {fileImg && ( | ||
| <div className={styles.addItemImagePreview}> | ||
| <img src={fileImg ? URL.createObjectURL(fileImg) : ''} alt='이미지 미리보기' /> | ||
| <button type='button' onClick={handleRemoveFile}> | ||
| <img src='/images/common/ic_tag_x.svg' alt='이미지 삭제' /> | ||
| </button> | ||
| </div> | ||
| )} | ||
| </div> | ||
| {showFileLimitAlert && <span className={styles.addItemImageAlert}>*이미지 등록은 최대 1개까지 가능합니다.</span>} | ||
| </> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,86 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import styles from '../styles/AddItemLists.module.css'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import AddItemImage from './AddItemImage'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import AddItemTag from './AddItemTag'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState, useRef } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❗️ 수정요청
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useAddItemForm } from '@/contexts/AddItemFormContext'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function AddItemsLists() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const inputRef = useRef(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { name, setName, description, setDescription, price, setPrice } = useAddItemForm(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handlePriceChange = (e) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const el = inputRef.current; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rawValue = e.target.value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cursorPos = el.selectionStart; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 쉼표 제거 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cleanValue = rawValue.replace(/[^0-9]/g, ''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const numericValue = cleanValue === '' ? 0 : Number(cleanValue); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 상태 업데이트 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| changePrice(numericValue, rawValue, cursorPos); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const changePrice = (value, rawValue, prevCursorPos) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💊 제안 const changePrice = (value, rawValue, prevCursorPos) => {
const formatted = value ? value.toLocaleString() : "";
setPrice(formatted);
setTimeout(() => {
...
// 새 포맷된 문자열 <= ❗️ 불필요
// const formatted = value === 0 ? '' : value.toLocaleString();
for (let i = 0; i < formatted.length; i++) { ... }
}
}또한 setTimeout에 콜백도 따로 이름을 붙여 분리해주시면 가독성에 더 좋을 것 같습니다~ |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setPrice(value === 0 ? '' : value.toLocaleString()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+24
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💊 제안
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 커서 복원 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!inputRef.current) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 새 포맷된 문자열 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const formatted = value === 0 ? '' : value.toLocaleString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 이전 값에서 커서까지 몇 개의 숫자가 있었는지 계산 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const numbersBeforeCursor = rawValue.slice(0, prevCursorPos).replace(/[^0-9]/g, '').length; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 새 포맷 문자열에서 그 숫자 위치를 다시 찾음 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let newCursorPos = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let digitsSeen = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < formatted.length; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (/\d/.test(formatted[i])) digitsSeen++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (digitsSeen === numbersBeforeCursor) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| newCursorPos = i + 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inputRef.current.setSelectionRange(newCursorPos, newCursorPos); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <li className={styles.addItemListItem}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p>상품 이미지</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <AddItemImage /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <li className={styles.addItemListItem}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label htmlFor='name'>상품명</label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input type='text' name='name' id='name' placeholder='상품명을 입력해주세요' value={name} onChange={(e) => setName(e.target.value)} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <li className={styles.addItemListItem}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label htmlFor='description'>상품 소개</label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <textarea name='description' id='description' placeholder='상품 소개를 입력해주세요' value={description} onChange={(e) => setDescription(e.target.value)} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <li className={styles.addItemListItem}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label htmlFor='price'>판매가격</label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={inputRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type='text' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name='price' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id='price' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder='판매 가격을 입력해주세요' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={price} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={handlePriceChange} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <li className={styles.addItemListItem}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <AddItemTag /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❗️ 수정요청 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import styles from '../styles/AddItemTag.module.css'; | ||
| import { useState } from 'react'; | ||
| import { useAddItemForm } from '@/contexts/AddItemFormContext'; | ||
|
|
||
| export default function AddItemTag() { | ||
| const { tags, setTags } = useAddItemForm(); | ||
| const [tag, setTag] = useState(''); | ||
| const handleTagChange = (e) => { | ||
| setTag(e.target.value); | ||
| } | ||
| const handleKeyDown = (e) => { | ||
| if (e.key === 'Enter') { | ||
| e.preventDefault(); // 폼 submit 방지 | ||
| const newTag = tag.trim(); // 공백 제거 | ||
| if (newTag.includes(' ')) { | ||
| alert('태그에는 띄어쓰기가 포함될 수 없습니다.'); | ||
| return; | ||
| } | ||
| if (newTag && !tags.includes(newTag)) { | ||
|
Comment on lines
+15
to
+19
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💊 제안 |
||
| // 태그 중복, 띄어쓰기 방지 | ||
| setTags([...tags, newTag]); | ||
| setTag(''); | ||
| } | ||
| } | ||
| }; | ||
| const handleTagDelete = (tag) => { | ||
| setTags([...tags].filter((ele) => ele !== tag)); | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <label htmlFor=''>태그</label> | ||
| <input | ||
| type='text' | ||
| name='' | ||
| id='' | ||
|
Comment on lines
+35
to
+36
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❗️ 수정요청 |
||
| placeholder='태그를 입력해주세요' | ||
| value={tag} | ||
| onChange={handleTagChange} | ||
| onKeyDown={handleKeyDown} | ||
| /> | ||
| <ul className={styles.addItemTagList}> | ||
| {tags.map((tag, index) => ( | ||
| <li key={`${tag}-${index}`}> | ||
| <span>#{tag}</span> | ||
| <button type='button' onClick={() => handleTagDelete(tag)}> | ||
| <img src='/images/common/ic_tag_x.svg' alt='삭제' /> | ||
| </button> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| </> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,39 @@ | ||
| import Footer from '@/components/Footer/Footer'; | ||
| import Header from '@/components/Header/Header'; | ||
| import styles from './styles/index.module.css'; | ||
| import CommonButton from '@/components/Common/CommonButton'; | ||
| import '@/styles/items.css'; | ||
| import AddItemLists from './components/AddItemLists'; | ||
| import { AddItemFormProvider, useAddItemForm } from '@/contexts/AddItemFormContext'; | ||
|
|
||
| export default function AddItem() { | ||
| return <div>AddItem</div>; | ||
| return ( | ||
| <> | ||
| <Header /> | ||
| <div id='container' className={`${styles.addItemPage} itemsPage`}> | ||
| <AddItemFormProvider> | ||
| <AddItemContents /> | ||
| </AddItemFormProvider> | ||
| </div> | ||
| <Footer /> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| function AddItemContents() { | ||
| const { isFormValid } = useAddItemForm(); | ||
| return ( | ||
| <form action='' className='inner04'> | ||
| <div className='contentHeader'> | ||
| <h3>상품 등록하기</h3> | ||
| <CommonButton | ||
| buttonType={{ buttonType: 'submit', buttonStyle: 'primary', buttonText: '등록' }} | ||
| disabled={!isFormValid} | ||
| /> | ||
| </div> | ||
| <ul className={styles.addItemList}> | ||
| <AddItemLists /> | ||
| </ul> | ||
| </form> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💬 여담
contextAPI를 사용하실 때는 주의해야할 점이 있습니다.
context를 통해 값을 전달할때는 해당 값이 변경되면 구독하는 컴포넌트에서 리렌더링이 발생할 수 있다는 것을 염두에 두고 작업을 해야합니다. (아래 코드의 경우 객체를 value로 공유하고 있어, 렌더링시 새로운 객체가 생성되어 참조가 변경되므로 object.is로 단순 비교시 변경으로 판단되어 리렌더링을 유발합니다. )
이를 해결하실 수 있는 다양한 방법이 있으나 가장 중요한 것은 동작에 대해 이해하는 것이므로 아래의 글들을 읽어보시는 것을 추천드립니다. 추후에는 상태관리 라이브러리를 사용해서 이러한 기능을 구현하실 가능성이 큰데 그때도 context 동작에 대해 이해하고 계시는 것이 도움이 되실거에요!
https://ko.react.dev/reference/react/useContext#optimizing-re-renders-when-passing-objects-and-functions
https://velog.io/@velopert/react-context-tutorial