+
)}
- {fieldConfig.hint !== "" && (
-
{hint}
- )}
+ {fieldConfig.hint !== "" &&
{hint}}
);
};
-export default Field;
+export default AuthField;
diff --git a/src/components/Field.module.css b/src/components/common/AuthField/AuthField.module.css
similarity index 100%
rename from src/components/Field.module.css
rename to src/components/common/AuthField/AuthField.module.css
diff --git a/src/components/common/FavoriteButton/FavoriteButton.jsx b/src/components/common/FavoriteButton/FavoriteButton.jsx
new file mode 100644
index 00000000..6bac4e51
--- /dev/null
+++ b/src/components/common/FavoriteButton/FavoriteButton.jsx
@@ -0,0 +1,16 @@
+import styles from "./FavoriteButton.module.css";
+
+const FavoriteButton = ({ favoriteCount }) => {
+ return (
+
+

+
{favoriteCount}
+
+ );
+};
+
+export default FavoriteButton;
diff --git a/src/components/common/FavoriteButton/FavoriteButton.module.css b/src/components/common/FavoriteButton/FavoriteButton.module.css
new file mode 100644
index 00000000..d77990ca
--- /dev/null
+++ b/src/components/common/FavoriteButton/FavoriteButton.module.css
@@ -0,0 +1,27 @@
+.favorite-button{
+ display: flex;
+ flex-direction: row;
+ gap:4px;
+ justify-content: center;
+ align-items: center;
+ border: 1px solid var(--color-gray300);
+ border-radius: 35px;
+ padding: 4px 12px;
+ cursor: pointer;
+}
+
+.favorite-button:hover {
+ background-color: var(--color-gray300);
+}
+
+.favorite-image {
+ aspect-ratio: 1/1;
+ width: 32px;
+}
+
+.favorite-count {
+ font-weight: 500;
+ font-size: 16px;
+ line-height: 26px;
+ color: var(--color-gray500);
+}
\ No newline at end of file
diff --git a/src/components/common/ItemCard/ItemCard.jsx b/src/components/common/ItemCard/ItemCard.jsx
new file mode 100644
index 00000000..b4050b85
--- /dev/null
+++ b/src/components/common/ItemCard/ItemCard.jsx
@@ -0,0 +1,36 @@
+import { useNavigate } from "react-router-dom";
+import { formatPriceKRW } from "../../../utils/format";
+import styles from "./ItemCard.module.css";
+import ItemImageViewer from "../ItemImageViewer/ItemImageViewer";
+
+const ItemCard = ({ id, imageUrl, name, price, favoriteCount }) => {
+ const navigate = useNavigate();
+ const handleItemCardClick = () => {
+ navigate(`./${id}`);
+ };
+
+ return (
+
+
+
+
{name}
+
{formatPriceKRW(price)}
+
+

+
{favoriteCount}
+
+
+
+ );
+};
+
+export default ItemCard;
diff --git a/src/components/ItemCard.module.css b/src/components/common/ItemCard/ItemCard.module.css
similarity index 97%
rename from src/components/ItemCard.module.css
rename to src/components/common/ItemCard/ItemCard.module.css
index 6e48859b..ed3db6c7 100644
--- a/src/components/ItemCard.module.css
+++ b/src/components/common/ItemCard/ItemCard.module.css
@@ -3,6 +3,7 @@
flex-direction: column;
gap: 16px;
width: 100%;
+ cursor: pointer;
}
.context {
diff --git a/src/components/ItemCardSkeleton.jsx b/src/components/common/ItemCardSkeleton/ItemCardSkeleton.jsx
similarity index 100%
rename from src/components/ItemCardSkeleton.jsx
rename to src/components/common/ItemCardSkeleton/ItemCardSkeleton.jsx
diff --git a/src/components/ItemCardSkeleton.module.css b/src/components/common/ItemCardSkeleton/ItemCardSkeleton.module.css
similarity index 100%
rename from src/components/ItemCardSkeleton.module.css
rename to src/components/common/ItemCardSkeleton/ItemCardSkeleton.module.css
diff --git a/src/components/common/ItemImageViewer/ItemImageViewer.jsx b/src/components/common/ItemImageViewer/ItemImageViewer.jsx
new file mode 100644
index 00000000..4216cc6a
--- /dev/null
+++ b/src/components/common/ItemImageViewer/ItemImageViewer.jsx
@@ -0,0 +1,27 @@
+import { useState } from "react";
+
+const IMAGE_DEFAULT_URL = "/images/img_items_default_md.png";
+
+const ItemImageViewer = ({ src, alt = "", defaultWidth = 100, borderRadius = 0 }) => {
+ const [isImageValid, setIsImageValid] = useState(true);
+
+ const imgSrc = isImageValid && src ? src : IMAGE_DEFAULT_URL;
+
+ const imageStyle = {
+ aspectRatio: "1 / 1",
+ borderRadius: borderRadius,
+ width: "100%",
+ };
+
+ return (
+

setIsImageValid(false)}
+ width={defaultWidth}
+ />
+ );
+};
+
+export default ItemImageViewer;
diff --git a/src/components/common/ItemImageViewer/ItemImageViewer.module.css b/src/components/common/ItemImageViewer/ItemImageViewer.module.css
new file mode 100644
index 00000000..38773874
--- /dev/null
+++ b/src/components/common/ItemImageViewer/ItemImageViewer.module.css
@@ -0,0 +1,4 @@
+.product-image {
+ aspect-ratio: 1/1;
+ border-radius: 28.59px;
+}
\ No newline at end of file
diff --git a/src/components/ItemsContainer.jsx b/src/components/common/ItemsContainer/ItemsContainer.jsx
similarity index 86%
rename from src/components/ItemsContainer.jsx
rename to src/components/common/ItemsContainer/ItemsContainer.jsx
index 21979297..692b9cfe 100644
--- a/src/components/ItemsContainer.jsx
+++ b/src/components/common/ItemsContainer/ItemsContainer.jsx
@@ -1,5 +1,5 @@
-import ItemCard from "./ItemCard";
-import ItemCardSkeleton from "./ItemCardSkeleton";
+import ItemCard from "../ItemCard/ItemCard";
+import ItemCardSkeleton from "../ItemCardSkeleton/ItemCardSkeleton";
import styles from "./ItemsContainer.module.css";
const ItemsContainer = ({ listName, itemList, pageSize }) => {
diff --git a/src/components/ItemsContainer.module.css b/src/components/common/ItemsContainer/ItemsContainer.module.css
similarity index 100%
rename from src/components/ItemsContainer.module.css
rename to src/components/common/ItemsContainer/ItemsContainer.module.css
diff --git a/src/components/common/KebabMenu/KebabMenu.jsx b/src/components/common/KebabMenu/KebabMenu.jsx
new file mode 100644
index 00000000..ee250dec
--- /dev/null
+++ b/src/components/common/KebabMenu/KebabMenu.jsx
@@ -0,0 +1,59 @@
+import { useEffect, useRef, useState } from "react";
+import styles from "./KebabMenu.module.css";
+const KebabMenu = ({ id, menuItems, onOpen = null, onClose = null }) => {
+ const [isKebabSelected, setIsKebabSelected] = useState();
+
+ const DropDownRef = useRef();
+ const kebabRef = useRef();
+
+ const handleDropDownOutsideClick = (e) => {
+ if (DropDownRef.current && DropDownRef.current.contains(e.target)) {
+ return;
+ } else if (kebabRef.current.contains(e.target)) {
+ return;
+ } else {
+ setIsKebabSelected(false);
+ }
+ };
+
+ const handleKebabClick = () => {
+ setIsKebabSelected(!isKebabSelected);
+ };
+
+ useEffect(() => {
+ if (!isKebabSelected) {
+ if (onClose) onClose();
+ document.removeEventListener("mousedown", handleDropDownOutsideClick);
+ } else {
+ if (onOpen) onOpen();
+ document.addEventListener("mousedown", handleDropDownOutsideClick);
+ }
+ return () => {
+ document.removeEventListener("mousedown", handleDropDownOutsideClick);
+ };
+ }, [isKebabSelected]);
+
+ return (
+
+

+ {isKebabSelected && (
+
+ {menuItems.map(({ label, onClick }) => (
+ -
+ {label}
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default KebabMenu;
diff --git a/src/components/common/KebabMenu/KebabMenu.module.css b/src/components/common/KebabMenu/KebabMenu.module.css
new file mode 100644
index 00000000..d1cb5a73
--- /dev/null
+++ b/src/components/common/KebabMenu/KebabMenu.module.css
@@ -0,0 +1,70 @@
+.container {
+ position: relative;
+}
+
+.kebab-button {
+ aspect-ratio: 1/1;
+ width: 24px;
+ cursor: pointer;
+ border-radius: 999px;
+}
+
+.kebab-button:hover {
+ background-color: var(--color-gray200);
+}
+
+.dropdown-menu {
+ display:flex;
+ flex-direction: column;
+ position: absolute;
+ right: 0;
+ top: 34px;
+ width: var(--dropdown-width);
+}
+
+.dropdown-button {
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 26px;
+ border: none;
+ border-left: 1px solid var(--color-gray300);
+ border-right: 1px solid var(--color-gray300);
+ background-color: #ffffff;
+ color: var(--color-gray500);
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 46px;
+}
+
+.dropdown-button:first-child {
+ border-top: 1px solid var(--color-gray300);
+ border-radius: 8px 8px 0 0;
+}
+
+.dropdown-button:last-child {
+ border-bottom: 1px solid var(--color-gray300);
+ border-radius: 0 0 8px 8px;
+}
+
+.dropdown-button:hover {
+ background-color: var(--color-gray100);
+}
+
+:root {
+ --dropdown-width: 102px;
+}
+
+/* Tablet */
+@media screen and (min-width: 768px) and (max-width: 1199px) {
+ :root {
+ --dropdown-width: 139px;
+ }
+}
+/* PC */
+@media (min-width: 1200px) {
+ :root {
+ --dropdown-width: 139px;
+ }
+}
\ No newline at end of file
diff --git a/src/components/common/Pagination/Pagination.jsx b/src/components/common/Pagination/Pagination.jsx
new file mode 100644
index 00000000..ed30edbe
--- /dev/null
+++ b/src/components/common/Pagination/Pagination.jsx
@@ -0,0 +1,37 @@
+import styles from "./Pagination.module.css";
+import PaginationButton from "./PaginationButton";
+
+const Pagination = ({
+ currentPageNumber,
+ visiblePageNumbers,
+ paginationHandler,
+ paginationState,
+}) => {
+ return (
+
+ );
+};
+
+export default Pagination;
diff --git a/src/components/common/Pagination/Pagination.module.css b/src/components/common/Pagination/Pagination.module.css
new file mode 100644
index 00000000..04755720
--- /dev/null
+++ b/src/components/common/Pagination/Pagination.module.css
@@ -0,0 +1,11 @@
+.pagination-container {
+ display: flex;
+ flex-direction: row;
+ padding-top: var(--items-pagination-padding-top);
+ padding-bottom: var(--items-pagination-padding-bottom);
+ padding: 43px 0 58px;
+ justify-content: center;
+ align-items: center;
+ background-color: var(--color-gray050);
+ gap: 4px;
+}
\ No newline at end of file
diff --git a/src/components/common/Pagination/PaginationButton.jsx b/src/components/common/Pagination/PaginationButton.jsx
new file mode 100644
index 00000000..4a082532
--- /dev/null
+++ b/src/components/common/Pagination/PaginationButton.jsx
@@ -0,0 +1,45 @@
+import styles from "./PaginationButton.module.css";
+
+const ICON_TYPE_ENABLE_SRC = {
+ prev: {
+ false: "/images/ic_prevPageClick_inactive.png",
+ true: "/images/ic_prevPageClick_active.png",
+ },
+ number: {
+ false: null,
+ true: null,
+ },
+ next: {
+ false: "/images/ic_nextPageClick_inactive.png",
+ true: "/images/ic_nextPageClick_active.png",
+ },
+};
+
+const PaginationButton = ({
+ type,
+ pageNumber = null,
+ onClick,
+ isEnabled = true,
+ currentPageNumber = 0,
+}) => {
+ const ButtonClassName = currentPageNumber === pageNumber ? "selected" : "";
+ return (
+
+ );
+};
+
+export default PaginationButton;
diff --git a/src/components/Pagination.module.css b/src/components/common/Pagination/PaginationButton.module.css
similarity index 62%
rename from src/components/Pagination.module.css
rename to src/components/common/Pagination/PaginationButton.module.css
index 9ea6a535..a9a76c72 100644
--- a/src/components/Pagination.module.css
+++ b/src/components/common/Pagination/PaginationButton.module.css
@@ -1,15 +1,3 @@
-.pagination-container {
- display: flex;
- flex-direction: row;
- padding-top: var(--items-pagination-padding-top);
- padding-bottom: var(--items-pagination-padding-bottom);
- padding: 43px 0 58px;
- justify-content: center;
- align-items: center;
- background-color: var(--color-gray050);
- gap: 4px;
-}
-
.pagination-button {
display: flex;
flex-direction: row;
diff --git a/src/components/common/SearchInput/SearchInput.jsx b/src/components/common/SearchInput/SearchInput.jsx
new file mode 100644
index 00000000..dd1d6104
--- /dev/null
+++ b/src/components/common/SearchInput/SearchInput.jsx
@@ -0,0 +1,22 @@
+import styles from "./SearchInput.module.css";
+
+const SearchInput = ({ inputValue, onInputChange, onInputEnterPress }) => {
+ return (
+
+

+
+
+ );
+};
+
+export default SearchInput;
diff --git a/src/components/common/SearchInput/SearchInput.module.css b/src/components/common/SearchInput/SearchInput.module.css
new file mode 100644
index 00000000..2bb2970b
--- /dev/null
+++ b/src/components/common/SearchInput/SearchInput.module.css
@@ -0,0 +1,29 @@
+.search-input-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ background-color: var(--color-gray200);
+ width: var(--search-input-container-width);
+ padding: 0 9px;
+ height: 42px;
+ border-radius: 12px;
+ flex-grow: var(--search-input-flex-grow);
+}
+
+.search-input-icon {
+ aspect-ratio: 1/1;
+}
+
+.search-input {
+ border: none;
+ background-color: var(--color-gray200);
+ flex-grow: 1;
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 26px;
+ outline: none;
+}
+
+.search-input::placeholder {
+ color: var(--color-gray400);
+}
\ No newline at end of file
diff --git a/src/components/common/SelectDropdown/SelectDropdown.jsx b/src/components/common/SelectDropdown/SelectDropdown.jsx
new file mode 100644
index 00000000..3dfca321
--- /dev/null
+++ b/src/components/common/SelectDropdown/SelectDropdown.jsx
@@ -0,0 +1,70 @@
+import { useEffect, useRef, useState } from "react";
+import styles from "./SelectDropdown.module.css";
+
+const SelectDropDown = ({ dropdownItems, setKey }) => {
+ const [isSelected, setIsSelected] = useState(false);
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const dropdownButtonRef = useRef();
+ const dropdownListRef = useRef();
+
+ const handleDropDownButtonClick = () => {
+ setIsSelected(!isSelected);
+ };
+
+ const handleDropDownOutsideClick = (e) => {
+ if (dropdownListRef.current.contains(e.target)) {
+ return;
+ } else if (dropdownButtonRef.current.contains(e.target)) {
+ return;
+ } else {
+ setIsSelected(false);
+ }
+ };
+
+ const handleDropDownListClick = (e) => {
+ setSelectedIndex(Number(e.currentTarget.dataset.index));
+ setKey(e.currentTarget.dataset.keyname);
+ setIsSelected(false);
+ };
+
+ useEffect(() => {
+ if (isSelected) {
+ document.addEventListener("mousedown", handleDropDownOutsideClick);
+ }
+ return () => {
+ document.removeEventListener("mousedown", handleDropDownOutsideClick);
+ };
+ }, [isSelected]);
+
+ return (
+
+
+
+ {dropdownItems[selectedIndex].label}
+
+
+
+ {isSelected && (
+
+ {dropdownItems.map(({ label, key }, index) => (
+ -
+ {label}
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default SelectDropDown;
diff --git a/src/components/common/SelectDropdown/SelectDropdown.module.css b/src/components/common/SelectDropdown/SelectDropdown.module.css
new file mode 100644
index 00000000..7cbf7612
--- /dev/null
+++ b/src/components/common/SelectDropdown/SelectDropdown.module.css
@@ -0,0 +1,98 @@
+.dropdown-container {
+ position: relative;
+ font-family: 'pretendard';
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 26px;
+}
+
+.dropdown-button-text {
+ display: var(--dropdown-button-text-display);
+}
+
+.dropdown-button {
+ display: flex;
+ flex-direction: row;
+ justify-content: var(--dropdown-button-justify-content);
+ align-items: center;
+ width: var(--dropdown-button-width);
+ padding: 0 var(--dropdown-button-padding-x);
+ height: 42px;
+ border: 1px solid var(--color-gray300);
+ border-radius: 12px;
+ background-color: var(--color-white);
+ cursor: pointer;
+}
+
+.dropdown-button:hover {
+ background-color: var(--color-gray300);
+}
+
+.dropdown-button-icon {
+ background-image: var(--dropdown-button-icon-src);
+ background-repeat: no-repeat;
+ background-size: cover;
+ aspect-ratio: 1/1;
+ width: 24px;
+}
+
+.dropdown-list {
+ position: absolute;
+ top: 52px;
+ cursor: pointer;
+}
+
+.dropdown-option {
+ width: 130px;
+ height: 42px;
+ border-left: 1px solid var(--color-gray300);
+ border-right: 1px solid var(--color-gray300);
+ border-bottom: 1px solid var(--color-gray300);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: var(--color-white);
+}
+
+.dropdown-option:first-child {
+ border-top: 1px solid var(--color-gray300);
+ border-radius: 12px 12px 0 0;
+}
+
+.dropdown-option:last-child {
+ border-radius: 0 0 12px 12px;
+}
+
+.dropdown-option:hover {
+ background-color: var(--color-gray300);
+}
+
+
+:root {
+ --dropdown-button-width: 42px;
+ --dropdown-button-text-display: none;
+ --dropdown-button-padding-x: 0;
+ --dropdown-button-justify-content: center;
+ --dropdown-button-icon-src: url('/images/ic_sort.png');
+}
+
+/* Tablet */
+@media screen and (min-width: 768px) and (max-width: 1199px) {
+ :root {
+ --dropdown-button-width: 130px;
+ --dropdown-button-text-display: inline;
+ --dropdown-button-padding-x: 20px;
+ --dropdown-button-justify-content: space-between;
+ --dropdown-button-icon-src: url('/images/ic_arrow_down.png');
+ }
+}
+/* PC */
+@media (min-width: 1200px) {
+ :root {
+ --dropdown-button-width: 130px;
+ --dropdown-button-text-display: inline;
+ --dropdown-button-padding-x: 20px;
+ --dropdown-button-justify-content: space-between;
+ --dropdown-button-icon-src: url('/images/ic_arrow_down.png');
+ }
+}
diff --git a/src/components/layout/LoadingSpinner/LoadingSpinner.jsx b/src/components/layout/LoadingSpinner/LoadingSpinner.jsx
new file mode 100644
index 00000000..2b9c317c
--- /dev/null
+++ b/src/components/layout/LoadingSpinner/LoadingSpinner.jsx
@@ -0,0 +1,22 @@
+import styles from "./LoadingSpinner.module.css";
+
+const LoadingSpinner = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LoadingSpinner;
diff --git a/src/components/layout/LoadingSpinner/LoadingSpinner.module.css b/src/components/layout/LoadingSpinner/LoadingSpinner.module.css
new file mode 100644
index 00000000..3251544b
--- /dev/null
+++ b/src/components/layout/LoadingSpinner/LoadingSpinner.module.css
@@ -0,0 +1,33 @@
+.container {
+ position: relative;
+ width: 80px;
+ height: 80px;
+}
+
+.dot {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 20px;
+ height: 8px;
+ background: #3498db;
+ border-radius: 20px;
+ transform: rotate(calc(30deg * var(--i))) translateX(40px);
+ animation: blink 1.2s linear infinite;
+ animation-delay: calc(var(--i) * 0.1s);
+}
+
+@keyframes rotate {
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes blink {
+ 0% {
+ opacity: 0.2;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
\ No newline at end of file
diff --git a/src/components/layout/LogoHeader/LogoHeader.jsx b/src/components/layout/LogoHeader/LogoHeader.jsx
new file mode 100644
index 00000000..9187ea77
--- /dev/null
+++ b/src/components/layout/LogoHeader/LogoHeader.jsx
@@ -0,0 +1,13 @@
+import { Link } from "react-router-dom";
+import styles from "./LogoHeader.module.css";
+
+const LogoHeader = () => {
+ return (
+
+

+
판다마켓
+
+ );
+};
+
+export default LogoHeader;
diff --git a/src/components/LogoHeader.module.css b/src/components/layout/LogoHeader/LogoHeader.module.css
similarity index 100%
rename from src/components/LogoHeader.module.css
rename to src/components/layout/LogoHeader/LogoHeader.module.css
diff --git a/src/components/Nav.jsx b/src/components/layout/Nav/Nav.jsx
similarity index 89%
rename from src/components/Nav.jsx
rename to src/components/layout/Nav/Nav.jsx
index 7eb5879a..172f9451 100644
--- a/src/components/Nav.jsx
+++ b/src/components/layout/Nav/Nav.jsx
@@ -1,6 +1,6 @@
import { Link } from "react-router-dom";
import styles from "./Nav.module.css";
-import { useIsLogin } from "../contexts/LoginStateContext";
+import { useLoginContext } from "../../../contexts/LoginContext";
const LINK_CLASSNAME = {
false: "",
@@ -8,7 +8,7 @@ const LINK_CLASSNAME = {
};
const Nav = ({ currentSection }) => {
- const isLogin = useIsLogin();
+ const { isLogin } = useLoginContext();
return (
@@ -16,7 +16,7 @@ const Nav = ({ currentSection }) => {
{