@@ -13,7 +13,7 @@ function InputItem({ title, placeholder, id, value, onChange, isTextArea }) {
value={value}
onChange={onChange}
placeholder={placeholder}
- className="input-description"
+ className={`input-description ${className}`}
/>
) : (
diff --git a/src/components/ui/Item/ItemCard.js b/src/components/ui/Item/ItemCard.js
index 21dbd6911..6d00f7b8b 100644
--- a/src/components/ui/Item/ItemCard.js
+++ b/src/components/ui/Item/ItemCard.js
@@ -1,21 +1,23 @@
import heartIcon from "../../../assets/icons/heart-icon.svg";
import defaultImg from "../../../assets/images/item-default-img-md.svg";
-
+import { Link } from "react-router-dom";
import "./ItemCard.css";
function ItemCard({ item }) {
return (
-
-

0 ? item.images[0] : defaultImg} className="itemCardThumbnail" />
-
-
{item.name}
-
{item.price.toLocaleString()}원
-
-

- {item.favoriteCount}
+
+
+

0 ? item.images[0] : defaultImg} className="itemCardThumbnail" />
+
+
{item.name}
+
{item.price.toLocaleString()}원
+
+

+ {item.favoriteCount}
+
-
+
);
}
diff --git a/src/components/ui/Nav/AllProductsNav.js b/src/components/ui/Nav/AllProductsNav.js
index 302873437..d7fadce2d 100644
--- a/src/components/ui/Nav/AllProductsNav.js
+++ b/src/components/ui/Nav/AllProductsNav.js
@@ -1,7 +1,7 @@
import { useState } from "react";
import { Link } from "react-router-dom";
+import Dropdown from "./Dropdown";
import "./AllProductsNav.css";
-import dropdownToggle from "../../../../src/assets/icons/dropdown-toggle.svg";
function AllProductsNav({ onSortChange }) {
const [isOpen, setIsOpen] = useState(false);
@@ -30,22 +30,7 @@ function AllProductsNav({ onSortChange }) {
-
-
- {isOpen && (
-
- - selectOption("최신순", "recent")}>
- 최신순
-
- - selectOption("인기순", "favorite")}>
- 인기순
-
-
- )}
-
+
);
diff --git a/src/components/ui/Nav/Dropdown.js b/src/components/ui/Nav/Dropdown.js
new file mode 100644
index 000000000..e412fe93e
--- /dev/null
+++ b/src/components/ui/Nav/Dropdown.js
@@ -0,0 +1,24 @@
+import dropdownToggle from "../../../../src/assets/icons/dropdown-toggle.svg";
+
+function Dropdown({ isOpen, selected, onToggle, onSelect }) {
+ return (
+
+
+ {isOpen && (
+
+ - onSelect("최신순", "recent")}>
+ 최신순
+
+ - onSelect("인기순", "favorite")}>
+ 인기순
+
+
+ )}
+
+ );
+}
+
+export default Dropdown;
diff --git a/src/hooks/useProductDetails.js b/src/hooks/useProductDetails.js
new file mode 100644
index 000000000..df2b516c2
--- /dev/null
+++ b/src/hooks/useProductDetails.js
@@ -0,0 +1,49 @@
+import { useState, useEffect } from "react";
+import { fetchProductById, fetchProductCommentById } from "../api/product";
+import { HttpException } from "../utils/exceptions";
+
+export function useProductDetails(productId) {
+ const [item, setItem] = useState(null);
+ const [comments, setComments] = useState([]);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ const getProductById = async (productId) => {
+ try {
+ const data = await fetchProductById(productId);
+ setItem(data);
+ } catch (error) {
+ if (error instanceof HttpException) {
+ setError(error.message);
+ } else {
+ setError("알 수 없는 오류가 발생했습니다.");
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getProductCommentById = async (productId) => {
+ try {
+ const { list } = await fetchProductCommentById(productId);
+ setComments(list);
+ } catch (error) {
+ if (error instanceof HttpException) {
+ setError(error.message);
+ } else {
+ setError("알 수 없는 오류가 발생했습니다");
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (productId) {
+ getProductById(productId);
+ getProductCommentById(productId);
+ }
+ }, [productId]);
+
+ return { item, comments, error, loading };
+}
diff --git a/src/pages/AddItemPage/AddItemPage.js b/src/pages/AddItemPage/AddItemPage.js
index 47d2819f5..8594c7eed 100644
--- a/src/pages/AddItemPage/AddItemPage.js
+++ b/src/pages/AddItemPage/AddItemPage.js
@@ -6,24 +6,40 @@ import InputItem from "../../components/ui/Input/InputItem";
import TagInput from "../../components/ui/Input/TagInput";
function AddItemPage() {
- const [name, setName] = useState("");
- const [description, setDescription] = useState("");
- const [price, setPrice] = useState();
- const [tags, setTags] = useState([]);
+ const [formValues, setFormValues] = useState({
+ name: "",
+ description: "",
+ price: null,
+ tags: [],
+ });
+
+ const { name, description, price, tags } = formValues;
const onAddTag = (tag) => {
if (!tags.includes(tag)) {
- setTags([...tags, tag]);
+ setFormValues((prev) => ({
+ ...prev,
+ tags: [...prev.tags, tag],
+ }));
}
};
const onRemoveTag = (tagToRemove) => {
- const updatedTags = tags.filter((tag) => tag !== tagToRemove);
- setTags(updatedTags);
+ setFormValues((prev) => ({
+ ...prev,
+ tags: prev.tags.filter((tag) => tag !== tagToRemove),
+ }));
};
const isFormValid = name && description && price && tags.length;
+ const handleInputChange = (field, value) => {
+ setFormValues((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
return (
@@ -39,14 +55,14 @@ function AddItemPage() {
id="name"
title="상품명"
value={name}
- onChange={(e) => setName(e.target.value)}
+ onChange={(e) => handleInputChange("name", e.target.value)}
placeholder="상품명을 입력해주세요"
/>
setDescription(e.target.value)}
+ onChange={(e) => handleInputChange("description", e.target.value)}
placeholder="상품 소개를 입력해주세요"
isTextArea
/>
@@ -54,7 +70,7 @@ function AddItemPage() {
id="price"
title="판매가격"
value={price}
- onChange={(e) => setPrice(e.target.value)}
+ onChange={(e) => handleInputChange("price", e.target.value)}
placeholder="판매 가격을 입력해주세요"
/>
+
+ 로딩 중...
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (!item) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ItemDetailPage;
diff --git a/src/utils/errorHandler.js b/src/utils/errorHandler.js
index f859a63df..d7f5f92a7 100644
--- a/src/utils/errorHandler.js
+++ b/src/utils/errorHandler.js
@@ -1,21 +1,24 @@
-const errorMessages = {
- 400: "잘못된 요청입니다.",
- 401: "인증이 필요합니다",
- 403: "접근 권한이 없습니다",
- 404: "요청한 상품을 찾을 수 없습니다",
-};
+import {
+ HttpException,
+ BadRequestException,
+ UnauthorizedException,
+ NotFoundException,
+ InternalServerErrorException,
+} from "./exceptions";
export const handleResponseError = (response) => {
- let errorMessage =
- errorMessages[response.status] || `클라이언트 오류가 발생했습니다. (에러 코드: ${response.status})`;
-
- if (response.status >= 500 && response.status < 600) {
- errorMessage = "서버에 오류가 발생했습니다. 잠시 후 다시 시도해주세요.";
- }
-
console.error(`HTTP Error: ${response.status} ${response.statusText}`);
- const error = new Error(errorMessage);
- error.statusCode = response.status;
- throw error;
+ switch (response.status) {
+ case 400:
+ throw new BadRequestException(response);
+ case 401:
+ throw new UnauthorizedException(response);
+ case 404:
+ throw new NotFoundException(response);
+ case 500:
+ throw new InternalServerErrorException(response);
+ default:
+ throw new HttpException(`오류가 발생했습니다. (에러 코드: ${response.status})`, response.status, response);
+ }
};
diff --git a/src/utils/exceptions.js b/src/utils/exceptions.js
new file mode 100644
index 000000000..cc51303e7
--- /dev/null
+++ b/src/utils/exceptions.js
@@ -0,0 +1,41 @@
+const HTTP_STATUS_CODE_MAPPER = {
+ 400: "잘못된 요청입니다.",
+ 401: "인증이 필요합니다",
+ 403: "접근 권한이 없습니다",
+ 404: "요청한 상품을 찾을 수 없습니다",
+ 500: "서버에 오류가 발생했습니다.",
+};
+
+export class HttpException extends Error {
+ constructor(message, statusCode, response) {
+ super(message);
+ this.name = this.constructor.name;
+ this.statusCode = statusCode;
+ this.response = response;
+ Error.captureStackTrace(this, this.constructor);
+ }
+}
+
+export class NotFoundException extends HttpException {
+ constructor(details) {
+ super(HTTP_STATUS_CODE_MAPPER[404], 404, details);
+ }
+}
+
+export class UnauthorizedException extends HttpException {
+ constructor(details) {
+ super(HTTP_STATUS_CODE_MAPPER[401], 401, details);
+ }
+}
+
+export class BadRequestException extends HttpException {
+ constructor(details) {
+ super(HTTP_STATUS_CODE_MAPPER[400], 400, details);
+ }
+}
+
+export class InternalServerErrorException extends HttpException {
+ constructor(details) {
+ super(HTTP_STATUS_CODE_MAPPER[500], 500, details);
+ }
+}
diff --git a/src/utils/formatRelativeTime.js b/src/utils/formatRelativeTime.js
new file mode 100644
index 000000000..81f82482d
--- /dev/null
+++ b/src/utils/formatRelativeTime.js
@@ -0,0 +1,27 @@
+export function formatRelativeTime(dateString) {
+ const now = new Date();
+ const past = new Date(dateString);
+ const diffInSeconds = Math.floor((now - past) / 1000);
+
+ if (diffInSeconds < 0) {
+ return "방금 전";
+ }
+
+ const intervals = [
+ { label: "년", seconds: 365 * 24 * 60 * 60 },
+ { label: "개월", seconds: 30 * 24 * 60 * 60 },
+ { label: "일", seconds: 24 * 60 * 60 },
+ { label: "시간", seconds: 60 * 60 },
+ { label: "분", seconds: 60 },
+ { label: "초", seconds: 1 },
+ ];
+
+ for (let interval of intervals) {
+ const count = Math.floor(diffInSeconds / interval.seconds);
+ if (count >= 1) {
+ return `${count}${interval.label} 전`;
+ }
+ }
+
+ return "방금 전";
+}