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
136 changes: 136 additions & 0 deletions src/pages/additem/Additem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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 { formatWithCommas } from "./utils/formatUtils";

export default function Additem() {
console.count('rendering')
const { validations } = useValidation();
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);
},
});

// 필수 필드만 모두 채워졌는지 확인
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={
getFieldValue("uploadImage") &&
URL.createObjectURL(getFieldValue("uploadImage"))
}
/>
{<p className={styles.errorText}>{errors.uploadImage || "\u00A0"}</p>}

<TextInputField
label="상품명"
name="productName"
onChange={handleChange("productName")}
onBlur={handleBlur("productName")}
placeholder="상품명을 입력해주세요"
error={errors.productName}
Copy link
Collaborator

Choose a reason for hiding this comment

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

모든 입력 필드가 하나의 부모 컴포넌트 안에 직접 렌더링됨

  • 부모 컴포넌트의 errors 상태가 변경되면 모든 자식 컴포넌트가 리렌더링됨
  • 각 필드가 독립적으로 리렌더링되지 않음

wrapperClass={styles.inputWrapper}
inputClass={styles.input}
errorClass={styles.errorText}
/>

<TextInputField
as="textarea"
label="상품 소개"
name="productDescription"
onChange={handleChange("productDescription")}
onBlur={handleBlur("productDescription")}
placeholder="상품 소개를 입력해주세요"
error={errors.productDescription}
wrapperClass={styles.inputWrapper}
textAreaClass={styles.textArea}
errorClass={styles.errorText}
/>

<TextInputField
label="상품 가격"
name="productPrice"
onChange={handleChange("productPrice")}
onBlur={handleBlur("productPrice", formatWithCommas)}
placeholder="숫자만 입력해주세요"
error={errors.productPrice}
wrapperClass={styles.inputWrapper}
inputClass={styles.input}
errorClass={styles.errorText}
/>

<TagInput
name="productTag"
label="태그"
tags={getFieldValue("productTag")}
// 태그 배열이 바뀔 때만 호출될 “상태 업데이트” 함수
setTags={(newTags) => {
setFieldValue("productTag", newTags);
}}
onChange={handleChange("productTag")}
onBlur={() =>
handleBlur("productTag")({
target: { value: getFieldValue("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;
}
73 changes: 73 additions & 0 deletions src/pages/additem/components/ImageUpload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useRef, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus, faTimesCircle } from "@fortawesome/free-solid-svg-icons";
import styles from "./ImageUpload.module.css"

export default function ImageUpload({ onImageChange }) {
const uploadImgRef = useRef();
const [previewUrl, setPreviewUrl] = useState(""); // 프리뷰 이미지 URL

// 이미지 등록 버튼을 누르면 숨겨져 있는 실제 file타입 input태그 클릭함
const handleUpload = () => {
if (uploadImgRef.current) {
uploadImgRef.current.value = ""; // ← 같은 파일도 다시 선택 가능하게
uploadImgRef.current.click();
}
};
// URL.createObjectURL(file)로 임시 URL 생성해서 프리뷰 띄우는 함수
const handleFileChange = () => {
const file = uploadImgRef.current?.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
setPreviewUrl(url);
onImageChange(file, url); // 부모에게 전달
}
};
// 프리뷰 이미지 닫기 함수
const handleRemovePreviewButton = ()=>{
setPreviewUrl(null);
if (uploadImgRef.current) {
uploadImgRef.current.value = "";
}
onImageChange(null, null); // 삭제 알림
}

return (
<div className={styles.inputWrapper}>
<h3>상품 이미지</h3>
{/* 안 보이게 숨겨져 있음 */}
<input
type="file"
accept="image/*"
ref={uploadImgRef}
name="imageUpload"
style={{ display: "none" }}
onChange={handleFileChange}
/>
{/* 실제로 보이는 부분 */}
<div className={styles.imageWrapper}>
<div className={styles.imageUploadBox} onClick={handleUpload}>
<FontAwesomeIcon icon={faPlus} className={styles.plus} />
<div>이미지 등록</div>
</div>
{/* preview 이미지 */}
{previewUrl && (
<div className={styles.imagePreviewWrapper}>
<img
src={previewUrl}
alt="미리보기"
className={styles.imagePreview}
/>
<div
type="button"
onClick={handleRemovePreviewButton}
className={styles.removeImageButton}
>
<FontAwesomeIcon icon={faTimesCircle} />
</div>
</div>
)}
</div>
</div>
);
}
Loading
Loading