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
7 changes: 3 additions & 4 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Items from "./pages/Items/Items.js";
import Home from "./pages/home/Home.js";
import DefaultLayout from "./layouts/DefaultLayout";
import "pretendard/dist/web/static/pretendard.css";
import "./styles/global.css";
import Additem from "./pages/Additem";
import Additem from "./pages/additem/Additem.js";
import Items from "./pages/Items/Items.js";

function App() {

return (
<div className="app">
<Routes>
Expand Down
5 changes: 2 additions & 3 deletions src/layouts/DefaultLayout.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Outlet } from "react-router-dom";
import Navbar from "../components/Navbar/Navbar.js";
import styles from "./DefaultLayout.module.css"

import styles from "./DefaultLayout.module.css";
import Navbar from "../components/Navbar/Navbar";

function DefaultLayout({ children }) {
return (
Expand Down
6 changes: 0 additions & 6 deletions src/pages/Additem.js

This file was deleted.

4 changes: 2 additions & 2 deletions src/pages/Items/Items.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import style from "./Items.module.css"
import BestProducts from "./components/BestProducts/BestProducts";
import AllProducts from "./components/AllProducts/AllProducts";
import BestProducts from "./components/best-products/BestProducts";
import AllProducts from "./components/all-products/AllProducts";
import { BEST_PRODUCTS_PER_DEVICE,ALL_PRODUCTS_PER_DEVICE } from "../../constants/products";
import { TITLE_ALL_PRODUCTS_COMP,TITLE_BEST_PRODUCTS_COMP } from "../../constants/titles";
function Items() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import styles from "./AllProducts.module.css";
import useProductsPagination from "../../hooks/useProductsPagination";
import ProductSection from "../ProductSection/ProductSection";
import ProductSection from "../product-section/ProductSection";
import Pagination from "../Pagination/Pagination";

function AllProducts({ title, itemsPerDevice }) {
const { products, totalPages, page, changePage, sort, handleSortChange, } =
const { products, totalPages, page, changePage, sort, handleSortChange } =
useProductsPagination(itemsPerDevice);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { fetchProducts } from "../../../../api/products";
import { getLimitFromWindowWidth } from "../../../../utils/getLimitFromWindowWidth";
import ProductSection from "../ProductSection/ProductSection";
import ProductSection from "../product-section/ProductSection";

function BestProducts({ title, itemsPerDevice }) {
const [bestProducts, setBestProducts] = useState([]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import style from "./ProductSection.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import SortSelect from "../SortSelect/SortSelect";
import ProductCard from "../ProductCard/ProductCard";
import SortSelect from "../sort-select/SortSelect";
import ProductCard from "../product-card/ProductCard";
import { useNavigate } from "react-router-dom";

function ProductSection({
Expand Down
148 changes: 148 additions & 0 deletions src/pages/additem/Additem.js
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;
};
Comment on lines +35 to +44
Copy link
Collaborator

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 함수를 사용해주는 역할로 축소되어 역할이 명확해지고, 일관성이 생기겠네요 :)

예시)

  const handlePriceChange = useCallback(
    (e) => {
      const formatted = formatWithCommas(e.target.value);
      setFieldValue("productPrice", formatted);
    },
    [setFieldValue]
  );


// 필수 필드만 모두 채워졌는지 확인
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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
const isSubmitEnabled =
data.productName.trim() !== "" &&
data.productDescription.trim() !== "" &&
data.productPrice.trim() !== "" &&
Array.isArray(data.productTag) &&
data.productTag.length > 0;
const isSubmitEnabled =
getFieldValue("productName")?.trim() !== "" &&
getFieldValue("productDescription")?.trim() !== "" &&
getFieldValue("productPrice")?.trim() !== "" &&
Array.isArray(getFieldValue("productTag")) &&
getFieldValue("productTag").length > 0;


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}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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>
);
}
99 changes: 99 additions & 0 deletions src/pages/additem/Additem.module.css
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;
}
Loading
Loading