-
Notifications
You must be signed in to change notification settings - Fork 39
[김진선] sprint6 #194
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\uC9C4\uC120-sprint6"
[김진선] sprint6 #194
Changes from all commits
9f0a3af
914477a
89f32f1
07f5161
b866e8a
be318b3
6adb3b2
e0287f3
9d3a9b6
baa3784
1e6f39b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,148 @@ | ||||||||||||||||||||||||||
| import styles from "./Additem.module.css"; | ||||||||||||||||||||||||||
| import { useForm } from "./hooks/useForm"; | ||||||||||||||||||||||||||
| import ImageUpload from "./components/ImageUpload"; | ||||||||||||||||||||||||||
| import TextInputField from "./components/TextInputField"; | ||||||||||||||||||||||||||
| import TagInput from "./components/TagInput"; | ||||||||||||||||||||||||||
| import { useValidation } from "./hooks/useValidation"; | ||||||||||||||||||||||||||
| import { useRef } from "react"; | ||||||||||||||||||||||||||
| import { formatWithCommas } from "./utils/formatUtils"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export default function Additem() { | ||||||||||||||||||||||||||
| const productNameRef = useRef(); | ||||||||||||||||||||||||||
| const productDescriptionRef = useRef(); | ||||||||||||||||||||||||||
| const productPriceRef = useRef(); | ||||||||||||||||||||||||||
| const { validations } = useValidation(); | ||||||||||||||||||||||||||
| const { data, errors, handleChange, handleBlur, handleSubmit } = useForm({ | ||||||||||||||||||||||||||
| initialValues: { | ||||||||||||||||||||||||||
| productName: "", | ||||||||||||||||||||||||||
| productDescription: "", | ||||||||||||||||||||||||||
| productPrice: "", | ||||||||||||||||||||||||||
| productTag: [], | ||||||||||||||||||||||||||
| uploadImage: null, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| validations, | ||||||||||||||||||||||||||
| onSubmit: (formData) => { | ||||||||||||||||||||||||||
| // 콤마 제거 + 숫자형으로 변환 | ||||||||||||||||||||||||||
| const numericPrice = Number(formData.productPrice.replace(/,/g, "")); | ||||||||||||||||||||||||||
| const payload = { | ||||||||||||||||||||||||||
| ...formData, | ||||||||||||||||||||||||||
| productPrice: numericPrice, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| console.log("제출 데이터:", payload); | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const handlePriceChange = (e) => { | ||||||||||||||||||||||||||
| const formatted = formatWithCommas(e.target.value); | ||||||||||||||||||||||||||
| // 1) React state 갱신 | ||||||||||||||||||||||||||
| handleChange( | ||||||||||||||||||||||||||
| "productPrice", | ||||||||||||||||||||||||||
| formatWithCommas | ||||||||||||||||||||||||||
| )({ target: { value: formatted } }); | ||||||||||||||||||||||||||
| // 2) DOM value도 즉시 갱신 | ||||||||||||||||||||||||||
| productPriceRef.current.value = formatted; | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 필수 필드만 모두 채워졌는지 확인 | ||||||||||||||||||||||||||
| const isSubmitEnabled = | ||||||||||||||||||||||||||
| data.productName.trim() !== "" && | ||||||||||||||||||||||||||
| data.productDescription.trim() !== "" && | ||||||||||||||||||||||||||
| data.productPrice.trim() !== "" && | ||||||||||||||||||||||||||
| Array.isArray(data.productTag) && | ||||||||||||||||||||||||||
| data.productTag.length > 0; | ||||||||||||||||||||||||||
|
Comment on lines
+47
to
+52
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. 위 코멘트처럼 getter함수를 useForm 훅에서 내려받는다면 이런 형태가 되겠죠? const {
errors,
handleChange,
handleBlur,
handleSubmit,
getFieldValue,
setFieldValue,
} = useForm({
initialValues: {
productName: "",
productDescription: "",
productPrice: "",
productTag: [],
uploadImage: null,
},
validations,
onSubmit: (formData) => {
// 콤마 제거 + 숫자형으로 변환
const numericPrice = Number(formData.productPrice.replace(/,/g, ""));
const payload = {
...formData,
productPrice: numericPrice,
};
console.log("제출 데이터:", payload);
},
});예시)
Suggested change
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| <form className={styles.form} onSubmit={handleSubmit}> | ||||||||||||||||||||||||||
| <header className={styles.header}> | ||||||||||||||||||||||||||
| <h1 className={styles.title}>상품 등록하기</h1> | ||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||
| type="submit" | ||||||||||||||||||||||||||
| className={styles.submitButton} | ||||||||||||||||||||||||||
| disabled={!isSubmitEnabled} | ||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||
| 등록 | ||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||
| </header> | ||||||||||||||||||||||||||
| <div className={styles.inputContainer}> | ||||||||||||||||||||||||||
| <ImageUpload | ||||||||||||||||||||||||||
| onImageChange={(file) => { | ||||||||||||||||||||||||||
| handleChange( | ||||||||||||||||||||||||||
| "uploadImage", | ||||||||||||||||||||||||||
| () => file | ||||||||||||||||||||||||||
| )({ target: { value: file } }); | ||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||
| previewUrl={data.uploadImage && URL.createObjectURL(data.uploadImage)} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
| {<p className={styles.errorText}>{errors.uploadImage || "\u00A0"}</p>} | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <TextInputField | ||||||||||||||||||||||||||
| label="상품명" | ||||||||||||||||||||||||||
| name="productName" | ||||||||||||||||||||||||||
| defaultValue="" | ||||||||||||||||||||||||||
| ref={productNameRef} | ||||||||||||||||||||||||||
|
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. 계속 이어서 피드백 드리자면, 여기서의 ref값은 필요없으니 날려줄수있고요! |
||||||||||||||||||||||||||
| onChange={handleChange("productName")} | ||||||||||||||||||||||||||
| onBlur={handleBlur("productName")} | ||||||||||||||||||||||||||
| placeholder="상품명을 입력해주세요" | ||||||||||||||||||||||||||
| error={errors.productName} | ||||||||||||||||||||||||||
| wrapperClass={styles.inputWrapper} | ||||||||||||||||||||||||||
| inputClass={styles.input} | ||||||||||||||||||||||||||
| errorClass={styles.errorText} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <TextInputField | ||||||||||||||||||||||||||
| as="textarea" | ||||||||||||||||||||||||||
| label="상품 소개" | ||||||||||||||||||||||||||
| name="productDescription" | ||||||||||||||||||||||||||
| defaultValue="" | ||||||||||||||||||||||||||
| ref={productDescriptionRef} | ||||||||||||||||||||||||||
| onChange={handleChange("productDescription")} | ||||||||||||||||||||||||||
| onBlur={handleBlur("productDescription")} | ||||||||||||||||||||||||||
| placeholder="상품 소개를 입력해주세요" | ||||||||||||||||||||||||||
| error={errors.productDescription} | ||||||||||||||||||||||||||
| wrapperClass={styles.inputWrapper} | ||||||||||||||||||||||||||
| textAreaClass={styles.textArea} | ||||||||||||||||||||||||||
| errorClass={styles.errorText} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <TextInputField | ||||||||||||||||||||||||||
| label="상품 가격" | ||||||||||||||||||||||||||
| name="productPrice" | ||||||||||||||||||||||||||
| defaultValue="" | ||||||||||||||||||||||||||
| ref={productPriceRef} | ||||||||||||||||||||||||||
| onChange={handlePriceChange} | ||||||||||||||||||||||||||
| onBlur={handleBlur("productPrice")} | ||||||||||||||||||||||||||
| placeholder="숫자만 입력해주세요" | ||||||||||||||||||||||||||
| error={errors.productPrice} | ||||||||||||||||||||||||||
| wrapperClass={styles.inputWrapper} | ||||||||||||||||||||||||||
| inputClass={styles.input} | ||||||||||||||||||||||||||
| errorClass={styles.errorText} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <TagInput | ||||||||||||||||||||||||||
| name="productTag" | ||||||||||||||||||||||||||
| label="태그" | ||||||||||||||||||||||||||
| tags={data.productTag} | ||||||||||||||||||||||||||
| // 태그 배열이 바뀔 때만 호출될 “상태 업데이트” 함수 | ||||||||||||||||||||||||||
| setTags={(newTags) => | ||||||||||||||||||||||||||
| // newTags 배열을 e.target.value처럼 흉내내서 useForm의 handleChange 실행 | ||||||||||||||||||||||||||
| handleChange( | ||||||||||||||||||||||||||
| "productTag", | ||||||||||||||||||||||||||
| () => newTags | ||||||||||||||||||||||||||
| )({ | ||||||||||||||||||||||||||
| target: { value: newTags }, | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| onChange={handleChange("productTag")} | ||||||||||||||||||||||||||
| onBlur={handleBlur("productTag")} | ||||||||||||||||||||||||||
| error={errors.productTag} | ||||||||||||||||||||||||||
| wrapperClass={styles.inputWrapper} | ||||||||||||||||||||||||||
| inputClass={styles.input} | ||||||||||||||||||||||||||
| errorClass={styles.errorText} | ||||||||||||||||||||||||||
| tagContainerClass={styles.tagsContainer} | ||||||||||||||||||||||||||
| tagClass={styles.tag} | ||||||||||||||||||||||||||
| removeButtonClass={styles.removeTagButton} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| </form> | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| .form { | ||
| display: flex; | ||
| flex-direction: column; | ||
| color: var(--color-gray-800); | ||
| } | ||
|
|
||
| .title { | ||
| font-size: var(--font-size-md-lg); | ||
| font-weight: var(--font-weight-bold); | ||
| vertical-align: middle; | ||
| padding-bottom: 24px; | ||
| } | ||
|
|
||
| .header { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| } | ||
|
|
||
| .inputContainer { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 24px; | ||
| } | ||
|
|
||
| .inputWrapper { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 16px; | ||
| } | ||
|
|
||
| .submitButton { | ||
| background-color: #3692ff; | ||
| color: white; | ||
| padding: var(--form-submit-button); | ||
| border: none; | ||
| border-radius: 8px; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| .submitButton:disabled { | ||
| background-color: #ccc; | ||
| cursor: not-allowed; | ||
| } | ||
|
|
||
| .input{ | ||
| border: none; | ||
| border-radius: 12px; | ||
| color: #1F2937; | ||
| font-size: 16px; | ||
| font-weight: 400; | ||
| background-color: #F3F4F6; | ||
| height: 56px; | ||
| padding: 16px 24px; | ||
| } | ||
|
|
||
| .textArea{ | ||
| resize: none; | ||
| border: none; | ||
| border-radius: 12px; | ||
| color: #1F2937; | ||
| font-size: 16px; | ||
| font-weight: 400; | ||
| background-color: #F3F4F6; | ||
| height: var(--text-area-height); | ||
| padding: 16px 24px; | ||
| } | ||
|
|
||
| .errorText { | ||
| color: red; | ||
| font-size: 14px; | ||
| margin-top: 4px; | ||
| } | ||
|
|
||
| .tagsContainer { | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| } | ||
|
|
||
| .tag { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| padding: 6px 12px; | ||
| background-color: #F3F4F6; | ||
| border-radius: 26px; | ||
| margin-right: 8px; | ||
| margin-bottom: 8px; | ||
| font-size: 16px; | ||
| font-weight: 400; | ||
| } | ||
|
|
||
| .removeTagButton { | ||
| margin-left: 8px; | ||
| background: transparent; | ||
| border: none; | ||
| cursor: pointer; | ||
| font-weight: bold; | ||
| } |
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.
현재 data 상태와 ref를 동시에 사용하고 있어 일관성이 떨어지는 문제가 있습니다.
또한, 비제어 컴포넌트를 사용하면서도 ref로 DOM에 직접 접근하는 것은 설계상 모순입니다.
협업을 가정한다면 설계 의도에 따라 일정한 패턴으로 코드를 설계하는게 보다 중요해지는데요! 일관성을 위해 useForm 훅에서 ref값을 관리하는 역할을 일임하고, field value에 대한 ref를 생성해 getter와 setter를 추가해서 내려주는 방식은 어떨까요?
이렇게 개선한다면 현재 컴포넌트는 useForm에서 관리되는 ref값을 받아와 표시해주는 역할로 간소화되면서 모든 컴포넌트가 일관된 패턴으로 사용되고, 해당 라인에 있는 코드도 단순히 useForm에 추가된 setter 함수를 사용해주는 역할로 축소되어 역할이 명확해지고, 일관성이 생기겠네요 :)
예시)