@@ -107,11 +104,8 @@ export default function Home() {
/>
-
+
>
);
}
-const Container = styled.div`
- margin-top: ${({ theme }) => theme.spacing.header};
-`;
diff --git a/src/pages/Items/AllItemsSection.jsx b/src/pages/Items/AllItemsSection.jsx
deleted file mode 100644
index 14ddf030..00000000
--- a/src/pages/Items/AllItemsSection.jsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { useCallback, useEffect, useState } from 'react';
-import styled from 'styled-components';
-
-import { getProducts } from '@/apis/Items';
-import Button from '@/components/ui/Button';
-import useAsync from '@/hooks/useAsync';
-import useDebouncedResizeEffect from '@/hooks/useDebouncedResizeEffect';
-import useIsMobile from '@/hooks/useIsMobile';
-import { ORDER_BY } from '@/pages/Items/constants';
-import DropdownButton from '@/pages/Items/DropdownButton';
-import ItemBox from '@/pages/Items/ItemBox';
-import Pagination from '@/pages/Items/Pagination';
-import Search from '@/pages/Items/Search';
-import { getItemLimitByscreenSize } from '@/pages/Items/utils';
-import { device } from '@/styles/media';
-
-export default function AllItemsSection() {
- const [items, setItems] = useState([]);
- const [totalCount, setTotalCount] = useState(1);
- const [orderBy, setOrderBy] = useState(ORDER_BY.RECENT);
- const [searchInput, setSearchInput] = useState('');
- const [page, setPage] = useState(1);
- const [pageSize, setPageSize] = useState(
- getItemLimitByscreenSize({
- mobile: 4,
- tablet: 6,
- desktop: 10,
- })
- );
- const [isLoading, loadingError, getProductsAsync] = useAsync(getProducts);
- const isMobile = useIsMobile();
-
- const handleLoad = useCallback(
- async (options) => {
- const result = await getProductsAsync(options);
- if (!result) return; //error
- setItems(result?.list);
- setTotalCount(result?.totalCount);
- },
- [getProductsAsync]
- );
-
- useEffect(() => {
- handleLoad({ orderBy, page, pageSize, keyword: searchInput });
- }, [handleLoad, orderBy, page, pageSize, searchInput]);
-
- useDebouncedResizeEffect(() => {
- setPageSize(
- getItemLimitByscreenSize({
- mobile: 4,
- tablet: 6,
- desktop: 10,
- })
- );
- });
-
- return (
-
-
-
- 전체 상품
- {isMobile ? (
-
- ) : (
-
- )}
-
-
- {isMobile ? (
-
- ) : (
-
- )}
-
-
-
-
- {items.map((item) => (
-
- ))}
-
-
-
- );
-}
-const Section = styled.section`
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- gap: ${({ theme }) => theme.spacing.lg};
-`;
-const Head = styled.div`
- display: grid;
- grid-template-rows: 1fr 1fr;
- gap: ${({ theme }) => theme.spacing.sm};
- @media ${device.TABLET} {
- display: flex;
- & > div:first-child {
- flex-grow: 1;
- }
- & > div:last-child {
- justify-content: flex-end;
- }
- }
- @media ${device.DESKTOP} {
- }
-`;
-const Title = styled.h2`
- font-size: ${({ theme }) => theme.fontSize.md};
- font-weight: 700;
-`;
-const Control = styled.div`
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: ${({ theme }) => theme.spacing.sm};
- & > form {
- flex: 1 1;
- }
-`;
-
-const Items = styled.ul`
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: ${({ theme }) => theme.spacing['2xl']} ${({ theme }) => theme.spacing.lg};
- @media ${device.TABLET} {
- grid-template-columns: repeat(3, 1fr);
- }
- @media ${device.DESKTOP} {
- grid-template-columns: repeat(5, 1fr);
- }
-`;
diff --git a/src/pages/Items/BestItemsSection.jsx b/src/pages/Items/BestItemsSection.jsx
deleted file mode 100644
index e271df96..00000000
--- a/src/pages/Items/BestItemsSection.jsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import { useCallback, useEffect, useState } from 'react';
-import styled from 'styled-components';
-
-import { getProducts } from '@/apis/Items';
-import useAsync from '@/hooks/useAsync';
-import useDebouncedResizeEffect from '@/hooks/useDebouncedResizeEffect';
-import { ORDER_BY } from '@/pages/Items/constants';
-import ItemBox from '@/pages/Items/ItemBox';
-import { getItemLimitByscreenSize } from '@/pages/Items/utils';
-import { device } from '@/styles/media';
-
-const _BEST_ITEMS_DEFAULT_VALUES = {
- page: 1,
- pageSize: 5,
- orderBy: ORDER_BY.FAVORITE,
- keyword: '',
-};
-
-export default function BestItemsSection() {
- const [items, setItems] = useState([]);
- const [pageSize, setPageSize] = useState(
- getItemLimitByscreenSize({
- mobile: 1,
- tablet: 2,
- desktop: 4,
- })
- );
- const [isLoading, loadingError, getProductsAsync] = useAsync(getProducts);
- const handleLoad = useCallback(
- async (options) => {
- const result = await getProductsAsync(options);
- if (!result) return; //error
- setItems(result?.list);
- },
- [getProductsAsync]
- );
- useEffect(() => {
- handleLoad({ ..._BEST_ITEMS_DEFAULT_VALUES, pageSize });
- }, [handleLoad, pageSize]);
-
- useDebouncedResizeEffect(() => {
- setPageSize(
- getItemLimitByscreenSize({
- mobile: 1,
- tablet: 2,
- desktop: 4,
- })
- );
- });
-
- return (
-
- 베스트 상품
-
- {items.map((item) => (
-
- ))}
-
-
- );
-}
-const Section = styled.section`
- width: 100%;
-`;
-const Title = styled.h2`
- margin-bottom: ${({ theme }) => theme.spacing.lg};
- font-size: ${({ theme }) => theme.fontSize.md};
- font-weight: 700;
-`;
-
-const Items = styled.ul`
- display: grid;
- grid-template-columns: 1fr;
- gap: 15px;
- @media ${device.TABLET} {
- grid-template-columns: repeat(2, 1fr);
- }
- @media ${device.DESKTOP} {
- grid-template-columns: repeat(4, 1fr);
- }
-`;
diff --git a/src/pages/Items/DropdownButton.jsx b/src/pages/Items/DropdownButton.jsx
deleted file mode 100644
index 1fe37c48..00000000
--- a/src/pages/Items/DropdownButton.jsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import { useState } from 'react';
-import styled from 'styled-components';
-
-import ArrowDownIcon from '@/assets/icons/ic_arrow_down.svg';
-import SortIcon from '@/assets/icons/ic_sort.svg';
-import useIsMobile from '@/hooks/useIsMobile';
-import { ORDER_BY } from '@/pages/Items/constants';
-import { device } from '@/styles/media';
-
-const _ORDER_BY_ENG_TO_KOR = {
- favorite: '인기순',
- recent: '최신순',
-};
-
-export default function DropdownButton({ orderBy, setOrderBy }) {
- const [isDropdownOpen, setIsDropdownOpen] = useState(false);
- const isMobile = useIsMobile();
- const handleClick = () => {
- setIsDropdownOpen((prev) => !prev);
- };
- const handleOptionClick = (e) => {
- setOrderBy(e.target.name);
- };
-
- return (
-
-
- {isMobile ? (
-
- ) : (
- <>
- {_ORDER_BY_ENG_TO_KOR[orderBy]}
-
- >
- )}
-
- {isDropdownOpen && (
-
-
-
-
- )}
-
- );
-}
-const Container = styled.div`
- font-weight: 400;
- font-size: ${({ theme }) => theme.fontSize.sm};
- position: relative;
-`;
-const Options = styled.div`
- position: absolute;
- top: 50px;
- right: 0px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- & > button:first-child {
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
- }
- & > button:last-child {
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- }
-`;
-const Option = styled.button`
- background-color: ${({ theme }) => theme.colors.white};
- border: 1px solid ${({ theme }) => theme.colors.gray200};
- border-radius: ${({ theme }) => theme.borderRadius.sm};
- padding: ${({ theme }) => theme.spacing.xs} ${({ theme }) => theme.spacing.sm};
- width: 8rem;
- padding: 12px 20px;
-`;
-const CurrentOption = styled(Option)`
- width: auto;
- display: flex;
- justify-content: space-between;
- align-items: center;
- @media ${device.TABLET} {
- width: 8rem;
- }
-`;
diff --git a/src/pages/Items/ItemBox.jsx b/src/pages/Items/ItemBox.jsx
deleted file mode 100644
index 5482ad7c..00000000
--- a/src/pages/Items/ItemBox.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import styled from 'styled-components';
-
-import LikeIcon from '@/assets/icons/ic_heart.svg';
-import ItemImg from '@/components/ui/ItemImg';
-
-export default function ItemBox({ title, price, like, imgUrl, imgAlt }) {
- const localePriceString = Number(price).toLocaleString('ko-KR');
- return (
-
-
- {title}
- {localePriceString}원
-
-
- {like}
-
-
- );
-}
-const Container = styled.li`
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- align-items: flex-start;
- gap: ${({ theme }) => theme.spacing.sm};
- width: 100%;
- color: ${({ theme }) => theme.colors.gray800};
-`;
-const Title = styled.span`
- margin-top: ${({ theme }) => theme.spacing.xs};
- font-size: ${({ theme }) => theme.fontSize.sm};
- font-weight: 500;
-`;
-const Price = styled.span`
- font-size: ${({ theme }) => theme.fontSize.sm};
- font-weight: 700;
-`;
-const LikeWrapper = styled.div`
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 4px;
- & > img {
- width: ${({ theme }) => theme.fontSize.sm};
- }
-`;
-const Like = styled.div`
- font-size: ${({ theme }) => theme.fontSize.xs};
- font-weight: 500;
- color: ${({ theme }) => theme.colors.gray600};
-`;
diff --git a/src/pages/Items/Pagination.jsx b/src/pages/Items/Pagination.jsx
deleted file mode 100644
index bf21ef28..00000000
--- a/src/pages/Items/Pagination.jsx
+++ /dev/null
@@ -1,86 +0,0 @@
-// @ts-nocheck
-import styled from 'styled-components';
-
-import LeftArrowIcon from '@/assets/icons/ic_arrow_left.svg';
-import RightArrowIcon from '@/assets/icons/ic_arrow_right.svg';
-
-export default function Pagination({ totalCount = 1, page, setPage }) {
- const pagesCount = Math.ceil(totalCount / 10);
- const pageGroup = Math.ceil(page / 5);
- const firstPage = (pageGroup - 1) * 5 + 1;
- const lastPage = pageGroup * 5;
- const countArray = Array.from({ length: pagesCount }, (v, i) => i + 1).slice(
- firstPage - 1,
- lastPage
- );
- const handleClick = (e) => {
- setPage(Number(e.target.value));
- };
- const handleLeftArrowClick = () => {
- setPage((prev) => {
- const next = prev - 1;
- return next >= 1 ? next : 1;
- });
- };
- const handleRightArrowClick = () => {
- setPage((prev) => {
- const next = prev + 1;
- return pagesCount >= next ? next : pagesCount;
- });
- };
- return (
-
-
-
-
-
-
- {countArray.map((count) => {
- return (
-
- {count}
-
- );
- })}
-
-
-
-
-
-
- );
-}
-const Container = styled.div`
- display: flex;
- justify-content: center;
- align-items: center;
- gap: ${({ theme }) => theme.spacing.xs};
- margin: ${({ theme }) => theme.spacing.lg} 0;
-`;
-const Counter = styled.button`
- display: flex;
- justify-content: center;
- align-items: center;
- background-color: ${({ $isactive, theme }) =>
- $isactive === 'true' ? theme.colors.primary : theme.colors.white};
- color: ${({ $isactive, theme }) =>
- $isactive === 'true' ? theme.colors.gray100 : theme.colors.gray500};
- border-radius: ${({ theme }) => theme.borderRadius.lg};
- border: 1px solid ${({ theme }) => theme.colors.gray200};
- width: 2.5rem;
- height: 2.5rem;
- padding: 12.5px;
- font-weight: 600;
- font-size: ${({ theme }) => theme.fontSize.sm};
-`;
-
-const IconWrapper = styled.div`
- width: 16px;
- height: 16px;
-`;
diff --git a/src/pages/Items/Search.tsx b/src/pages/Items/Search.tsx
deleted file mode 100644
index 5ee3ab81..00000000
--- a/src/pages/Items/Search.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import styled from 'styled-components';
-import SearchIcon from '@/assets/icons/ic_search.svg';
-
-export default function Search({ onSubmit }) {
- const handleSubmit = (e) => {
- e.preventDefault();
- onSubmit(e.target['search'].value);
- };
- return (
-
- );
-}
-const Form = styled.form`
- position: relative;
- max-width: 24rem;
- height: 2.625rem;
- & > svg {
- position: absolute;
- left: 10px;
- top: 10px;
- width: ${({ theme }) => theme.fontSize.lg};
- height: ${({ theme }) => theme.fontSize.lg};
- }
-`;
-const Input = styled.input`
- width: 100%;
- border-radius: ${({ theme }) => theme.borderRadius.sm};
- background-color: ${({ theme }) => theme.colors.gray100};
- padding: ${({ theme }) => theme.spacing.sm} 40px;
- &::placeholder {
- color: ${({ theme }) => theme.colors.gray400};
- }
-`;
diff --git a/src/pages/Items/components/AllItemsSection.jsx b/src/pages/Items/components/AllItemsSection.jsx
new file mode 100644
index 00000000..54b3ca44
--- /dev/null
+++ b/src/pages/Items/components/AllItemsSection.jsx
@@ -0,0 +1,87 @@
+import { useEffect, useState } from 'react';
+import { Link } from 'react-router-dom';
+
+import Button from '@/components/ui/Button';
+import Loading from '@/components/ui/Loading';
+import useDebouncedResizeEffect from '@/hooks/useDebouncedResizeEffect';
+import { useQuery } from '@/hooks/useFetch';
+import useIsMobile from '@/hooks/useIsMobile';
+import DropdownButton from '@/pages/Items/components/DropdownButton';
+import ItemBox from '@/pages/Items/components/ItemBox';
+import Pagination from '@/pages/Items/components/Pagination';
+import Search from '@/pages/Items/components/Search';
+import { getProducts } from '@/pages/Items/lib/api';
+import { ORDER_BY } from '@/pages/Items/lib/constants';
+import { getAllItemsLimitByScreenSize } from '@/pages/Items/lib/utils';
+import styles from '@/pages/Items/styles/AllItemsSection.module.scss';
+
+export default function AllItemsSection() {
+ const [totalCount, setTotalCount] = useState(1);
+ const [orderBy, setOrderBy] = useState(ORDER_BY.RECENT);
+ const [searchInput, setSearchInput] = useState('');
+ const [page, setPage] = useState(1);
+ const [pageSize, setPageSize] = useState(getAllItemsLimitByScreenSize());
+ const {
+ loading,
+ error,
+ data: items,
+ } = useQuery({
+ queryFn: () =>
+ getProducts({ orderBy, page, pageSize, keyword: searchInput }),
+ deps: [orderBy, page, pageSize, searchInput],
+ });
+
+ const isMobile = useIsMobile();
+
+ useEffect(() => {
+ setTotalCount(items?.totalCount);
+ }, [items]);
+
+ useDebouncedResizeEffect(() => {
+ setPageSize(getAllItemsLimitByScreenSize());
+ });
+
+ if (error) return
error
;
+ if (!items || loading) return
;
+ return (
+
+
+
+
전체 상품
+ {isMobile ? (
+
+ ) : (
+
+ )}
+
+
+ {isMobile ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {items?.list.map((item) => (
+
+
+
+ ))}
+
+
+
+ );
+}
+const ButtonToAddItemPage = () => (
+
+);
diff --git a/src/pages/Items/components/BestItemsSection.jsx b/src/pages/Items/components/BestItemsSection.jsx
new file mode 100644
index 00000000..11ab5107
--- /dev/null
+++ b/src/pages/Items/components/BestItemsSection.jsx
@@ -0,0 +1,53 @@
+import { useState } from 'react';
+import { Link } from 'react-router-dom';
+
+import Loading from '@/components/ui/Loading';
+import useDebouncedResizeEffect from '@/hooks/useDebouncedResizeEffect';
+import { useQuery } from '@/hooks/useFetch';
+import ItemBox from '@/pages/Items/components/ItemBox';
+import { getProducts } from '@/pages/Items/lib/api';
+import { ORDER_BY } from '@/pages/Items/lib/constants';
+import { getBestItemsLimitByScreenSize } from '@/pages/Items/lib/utils';
+import styles from '@/pages/Items/styles/BestItemsSection.module.scss';
+
+const _BEST_ITEMS_DEFAULT_VALUES = {
+ page: 1,
+ pageSize: 5,
+ orderBy: ORDER_BY.FAVORITE,
+ keyword: '',
+};
+export default function BestItemsSection() {
+ const [pageSize, setPageSize] = useState(getBestItemsLimitByScreenSize());
+ const {
+ loading,
+ error,
+ data: items,
+ } = useQuery({
+ queryFn: () => getProducts({ ..._BEST_ITEMS_DEFAULT_VALUES, pageSize }),
+ deps: [pageSize],
+ });
+ useDebouncedResizeEffect(() => {
+ setPageSize(getBestItemsLimitByScreenSize());
+ });
+
+ if (error) return
error
;
+ if (!items || loading) return
;
+ return (
+
+ 베스트 상품
+
+ {items?.list.map((item) => (
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/pages/Items/components/DropdownButton.jsx b/src/pages/Items/components/DropdownButton.jsx
new file mode 100644
index 00000000..bbb59c52
--- /dev/null
+++ b/src/pages/Items/components/DropdownButton.jsx
@@ -0,0 +1,56 @@
+import { useState } from 'react';
+
+import ArrowDownIcon from '@/assets/icons/ic_arrow_down.svg';
+import SortIcon from '@/assets/icons/ic_sort.svg';
+import useIsMobile from '@/hooks/useIsMobile';
+import { ORDER_BY } from '@/pages/Items/lib/constants';
+import styles from '@/pages/Items/styles/DropdownButton.module.scss';
+
+const _ORDER_BY_ENG_TO_KOR = {
+ favorite: '인기순',
+ recent: '최신순',
+};
+
+export default function DropdownButton({ orderBy, setOrderBy }) {
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+ const isMobile = useIsMobile();
+ const handleClick = () => {
+ setIsDropdownOpen((prev) => !prev);
+ };
+ const handleOptionClick = (e) => {
+ setOrderBy(e.target.name);
+ };
+
+ return (
+
+
+ {isDropdownOpen && (
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/pages/Items/components/ItemBox.jsx b/src/pages/Items/components/ItemBox.jsx
new file mode 100644
index 00000000..6d162765
--- /dev/null
+++ b/src/pages/Items/components/ItemBox.jsx
@@ -0,0 +1,18 @@
+import LikeIcon from '@/assets/icons/ic_heart.svg';
+import ItemImg from '@/components/ui/ItemImg';
+import styles from '@/pages/Items/styles/ItemBox.module.scss';
+
+export default function ItemBox({ title, price, like, imgUrl, imgAlt }) {
+ const localePriceString = Number(price).toLocaleString('ko-KR');
+ return (
+
+
+ {title}
+ {localePriceString}원
+
+
+ {like}
+
+
+ );
+}
diff --git a/src/pages/Items/components/Pagination.jsx b/src/pages/Items/components/Pagination.jsx
new file mode 100644
index 00000000..6c77beb5
--- /dev/null
+++ b/src/pages/Items/components/Pagination.jsx
@@ -0,0 +1,71 @@
+import classNames from 'classnames/bind';
+
+import LeftArrowIcon from '@/assets/icons/ic_arrow_left.svg';
+import RightArrowIcon from '@/assets/icons/ic_arrow_right.svg';
+import styles from '@/pages/Items/styles/Pagination.module.scss';
+
+export default function Pagination({ totalCount = 1, page, setPage }) {
+ const cn = classNames.bind(styles);
+ const isFirstPage = page === 1;
+ const pagesCount = Math.ceil(totalCount / 10);
+ const pageGroup = Math.ceil(page / 5);
+ const firstPage = (pageGroup - 1) * 5 + 1;
+ const lastPage = pageGroup * 5;
+ const countArray = Array.from({ length: pagesCount }, (v, i) => i + 1).slice(
+ firstPage - 1,
+ lastPage
+ );
+ const handleClick = (e) => {
+ setPage(Number(e.target.value));
+ };
+ const handleLeftArrowClick = () => {
+ setPage((prev) => {
+ const next = prev - 1;
+ return next >= 1 ? next : 1;
+ });
+ };
+ const handleRightArrowClick = () => {
+ setPage((prev) => {
+ const next = prev + 1;
+ return pagesCount >= next ? next : pagesCount;
+ });
+ };
+ return (
+
+
+ {countArray.map((count) => {
+ const isSamePage = count === page;
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/pages/Items/components/Search.tsx b/src/pages/Items/components/Search.tsx
new file mode 100644
index 00000000..0d28e834
--- /dev/null
+++ b/src/pages/Items/components/Search.tsx
@@ -0,0 +1,19 @@
+import SearchIcon from '@/assets/icons/ic_search.svg';
+import styles from '@/pages/Items/styles/Search.module.scss';
+
+export default function Search({ onSubmit }) {
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ onSubmit(e.target['search'].value);
+ };
+ return (
+
+ );
+}
diff --git a/src/pages/Items/index.jsx b/src/pages/Items/index.jsx
index e96c4b27..53bf0bb9 100644
--- a/src/pages/Items/index.jsx
+++ b/src/pages/Items/index.jsx
@@ -1,36 +1,14 @@
-import styled from 'styled-components';
-
-import Header from '@/components/layout/Header';
-import AllItemsSection from '@/pages/Items/AllItemsSection';
-import BestItemsSection from '@/pages/Items/BestItemsSection';
-import { device } from '@/styles/media';
+import AllItemsSection from '@/pages/Items/components/AllItemsSection';
+import BestItemsSection from '@/pages/Items/components/BestItemsSection';
+import styles from '@/pages/Items/styles/index.module.scss';
export default function Items() {
return (
<>
-
-
+
-
+
>
);
}
-
-const Container = styled.div`
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- gap: ${({ theme }) => theme.spacing['2xl']};
- padding: ${({ theme }) => `${theme.spacing['2xl']} ${theme.spacing.xl}`};
- margin-top: ${({ theme }) => theme.spacing.header};
- @media ${device.TABLET} {
- width: 100%;
- }
- @media ${device.DESKTOP} {
- margin-left: auto;
- margin-right: auto;
- padding: ${({ theme }) => `${theme.spacing['2xl']} 0`};
- max-width: 1200px;
- }
-`;
diff --git a/src/apis/Items.js b/src/pages/Items/lib/api.js
similarity index 87%
rename from src/apis/Items.js
rename to src/pages/Items/lib/api.js
index d73383a9..e0bb6892 100644
--- a/src/apis/Items.js
+++ b/src/pages/Items/lib/api.js
@@ -1,6 +1,6 @@
import { BASE_API_URL } from '@/apis/constants';
import { customFetch } from '@/apis/customFetch';
-import { DEFAULT_VALUES } from '@/pages/Items/constants';
+import { DEFAULT_VALUES } from '@/pages/Items/lib/constants';
export const getProducts = async ({
page = DEFAULT_VALUES.page,
diff --git a/src/pages/Items/constants.js b/src/pages/Items/lib/constants.js
similarity index 100%
rename from src/pages/Items/constants.js
rename to src/pages/Items/lib/constants.js
diff --git a/src/pages/Items/lib/utils.js b/src/pages/Items/lib/utils.js
new file mode 100644
index 00000000..843ea3e4
--- /dev/null
+++ b/src/pages/Items/lib/utils.js
@@ -0,0 +1,28 @@
+const screenSizeNumber = {
+ TABLET: 768,
+ DESKTOP: 1280,
+};
+const getItemLimitByscreenSize = ({
+ mobileLimit,
+ tabletLimit,
+ desktopLimit,
+}) => {
+ const width = window.innerWidth;
+ if (width > screenSizeNumber.DESKTOP) return desktopLimit;
+ if (width > screenSizeNumber.TABLET) return tabletLimit;
+ return mobileLimit;
+};
+export const getAllItemsLimitByScreenSize = () => {
+ return getItemLimitByscreenSize({
+ mobileLimit: 4,
+ tabletLimit: 6,
+ desktopLimit: 10,
+ });
+};
+export const getBestItemsLimitByScreenSize = () => {
+ return getItemLimitByscreenSize({
+ mobileLimit: 1,
+ tabletLimit: 2,
+ desktopLimit: 4,
+ });
+};
diff --git a/src/pages/Items/styles/AllItemsSection.module.scss b/src/pages/Items/styles/AllItemsSection.module.scss
new file mode 100644
index 00000000..e12d9958
--- /dev/null
+++ b/src/pages/Items/styles/AllItemsSection.module.scss
@@ -0,0 +1,44 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: var(--spacing-lg);
+ .sectionMenu {
+ display: grid;
+ grid-template-rows: 1fr 1fr;
+ gap: var(--spacing-md);
+ @include tablet {
+ display: flex;
+ & > div:first-child {
+ flex-grow: 1;
+ }
+ & > div:last-child {
+ justify-content: flex-end;
+ }
+ }
+ .menuWrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--spacing-sm);
+ & > form {
+ flex: 1 1;
+ }
+ .title {
+ font-size: var(--font-size-md);
+ font-weight: 700;
+ }
+ }
+ }
+ .itemList {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: var(--spacing-2xl) var(--spacing-lg);
+ @include tablet {
+ grid-template-columns: repeat(3, 1fr);
+ }
+ @include desktop {
+ grid-template-columns: repeat(5, 1fr);
+ }
+ }
+}
diff --git a/src/pages/Items/styles/BestItemsSection.module.scss b/src/pages/Items/styles/BestItemsSection.module.scss
new file mode 100644
index 00000000..fad3d041
--- /dev/null
+++ b/src/pages/Items/styles/BestItemsSection.module.scss
@@ -0,0 +1,20 @@
+.container {
+ width: 100%;
+
+ .title {
+ margin-bottom: var(--spacing-lg);
+ font-size: var(--font-size-md);
+ font-weight: 700;
+ }
+ .items {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: var(--spacing-md);
+ @include tablet {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ @include desktop {
+ grid-template-columns: repeat(4, 1fr);
+ }
+ }
+}
diff --git a/src/pages/Items/styles/DropdownButton.module.scss b/src/pages/Items/styles/DropdownButton.module.scss
new file mode 100644
index 00000000..558cc693
--- /dev/null
+++ b/src/pages/Items/styles/DropdownButton.module.scss
@@ -0,0 +1,47 @@
+@mixin option {
+ background-color: var(--color-white);
+ border: 1px solid var(--color-gray-200);
+ border-radius: var(--radius-sm);
+ padding: var(--spacing-sm) var(--spacing-lg);
+ width: 8rem;
+}
+
+.container {
+ font-weight: 400;
+ font-size: var(--font-size-sm);
+ position: relative;
+ .currentOption {
+ @include option;
+ width: auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ @include tablet {
+ width: 8rem;
+ }
+ }
+ .options {
+ @include flex-center;
+ flex-direction: column;
+ position: absolute;
+ top: rem(50px);
+ right: 0px;
+
+ & > button:first-child {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ & > button:last-child {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+ .option {
+ @include option;
+ &:hover {
+ background-color: var(--color-primary);
+ border: 1px solid var(--color-primary);
+ color: var(--color-white);
+ }
+ }
+ }
+}
diff --git a/src/pages/Items/styles/ItemBox.module.scss b/src/pages/Items/styles/ItemBox.module.scss
new file mode 100644
index 00000000..8c790bdd
--- /dev/null
+++ b/src/pages/Items/styles/ItemBox.module.scss
@@ -0,0 +1,32 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: var(--spacing-sm);
+ width: 100%;
+ color: var(--color-gray-800);
+ .title {
+ margin-top: var(--spacing-xs);
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ }
+ .price {
+ font-size: var(--font-size-sm);
+ font-weight: 700;
+ }
+ .likeWrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: rem(4px);
+ svg {
+ width: rem(16px);
+ }
+ .like {
+ font-size: var(--font-size-xs);
+ font-weight: 500;
+ color: var(--color-gray-600);
+ }
+ }
+}
diff --git a/src/pages/Items/styles/Pagination.module.scss b/src/pages/Items/styles/Pagination.module.scss
new file mode 100644
index 00000000..59d812bc
--- /dev/null
+++ b/src/pages/Items/styles/Pagination.module.scss
@@ -0,0 +1,27 @@
+.container {
+ @include flex-center;
+ gap: var(--spacing-xs);
+ margin: var(--spacing-lg) 0;
+}
+.counter {
+ @include flex-center;
+ background-color: var(--color-white);
+ color: var(--color-gray-500);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--color-gray-200);
+ width: 2.5rem;
+ height: 2.5rem;
+ padding: rem(12.5px);
+ font-weight: 600;
+ font-size: var(--font-size-sm);
+
+ &Active {
+ background-color: var(--color-primary);
+ color: var(--color-gray-100);
+ }
+
+ .iconWrapper {
+ width: 1rem;
+ height: 1rem;
+ }
+}
diff --git a/src/pages/Items/styles/Search.module.scss b/src/pages/Items/styles/Search.module.scss
new file mode 100644
index 00000000..eed1ee1d
--- /dev/null
+++ b/src/pages/Items/styles/Search.module.scss
@@ -0,0 +1,21 @@
+.form {
+ position: relative;
+ max-width: 24rem;
+ height: 2.625rem;
+ & > svg {
+ position: absolute;
+ left: rem(10px);
+ top: rem(10px);
+ width: rem(18px);
+ height: rem(18px);
+ }
+ .input {
+ width: 100%;
+ border-radius: var(--radius-sm);
+ background-color: var(--color-gray-100);
+ padding: var(--spacing-sm) rem(40px);
+ &::placeholder {
+ color: var(--color-gray-400);
+ }
+ }
+}
diff --git a/src/pages/Items/styles/index.module.scss b/src/pages/Items/styles/index.module.scss
new file mode 100644
index 00000000..d00cbd9f
--- /dev/null
+++ b/src/pages/Items/styles/index.module.scss
@@ -0,0 +1,16 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: var(--spacing-2xl);
+ padding: var(--spacing-2xl) var(--spacing-xl);
+ @include tablet {
+ width: 100%;
+ }
+ @include desktop {
+ margin-left: auto;
+ margin-right: auto;
+ padding: var(--spacing-2xl) 0;
+ max-width: var(--width-container);
+ }
+}
diff --git a/src/pages/Items/utils.js b/src/pages/Items/utils.js
deleted file mode 100644
index f25a8255..00000000
--- a/src/pages/Items/utils.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { screenSizeNumber } from '@/styles/media';
-
-export const getItemLimitByscreenSize = ({ mobile, tablet, desktop }) => {
- const width = window.innerWidth;
- if (width > screenSizeNumber.DESKTOP) return desktop;
- if (width > screenSizeNumber.TABLET) return tablet;
- return mobile;
-};
diff --git a/src/pages/Login.jsx b/src/pages/Login/index.jsx
similarity index 100%
rename from src/pages/Login.jsx
rename to src/pages/Login/index.jsx
diff --git a/src/pages/NotFound.jsx b/src/pages/NotFound/index.jsx
similarity index 100%
rename from src/pages/NotFound.jsx
rename to src/pages/NotFound/index.jsx
diff --git a/src/pages/Privacy.jsx b/src/pages/Privacy/index.jsx
similarity index 100%
rename from src/pages/Privacy.jsx
rename to src/pages/Privacy/index.jsx
diff --git a/src/pages/Product/components/AuthorInfo.jsx b/src/pages/Product/components/AuthorInfo.jsx
new file mode 100644
index 00000000..0d888640
--- /dev/null
+++ b/src/pages/Product/components/AuthorInfo.jsx
@@ -0,0 +1,40 @@
+import classNames from 'classnames/bind';
+
+import defaultProfileImg from '@/assets/imgs/default_profile.png';
+import styles from '@/pages/Product/styles/AuthorInfo.module.scss';
+
+// eslint-disable-next-line react-refresh/only-export-components
+export const AUTHOR_INFO_VARIANTS = {
+ product: 'product',
+ community: 'community',
+ comment: 'comment',
+};
+export default function AuthorInfo({
+ variant = AUTHOR_INFO_VARIANTS.product,
+ nickname,
+ updateAt,
+}) {
+ const cn = classNames.bind(styles);
+ return (
+
+

+
+ {nickname}
+ {updateAt}
+
+
+ );
+}
diff --git a/src/pages/Product/components/CommentForm.jsx b/src/pages/Product/components/CommentForm.jsx
new file mode 100644
index 00000000..350e9eb6
--- /dev/null
+++ b/src/pages/Product/components/CommentForm.jsx
@@ -0,0 +1,24 @@
+import { useState } from 'react';
+
+import Button from '@/components/ui/Button';
+import styles from '@/pages/Product/styles/CommentForm.module.scss';
+
+export default function CommentForm() {
+ const [input, setInput] = useState('');
+ const onChangeTextArea = (e) => setInput(e.target.value);
+ const hasInput = input.length > 0;
+ return (
+
+ );
+}
diff --git a/src/pages/Product/components/CommentSection.jsx b/src/pages/Product/components/CommentSection.jsx
new file mode 100644
index 00000000..bd8c9592
--- /dev/null
+++ b/src/pages/Product/components/CommentSection.jsx
@@ -0,0 +1,51 @@
+import { Fragment } from 'react';
+import { useParams } from 'react-router-dom';
+
+import EmptyImg from '@/assets/imgs/Img_inquiry_empty.png';
+import Loading from '@/components/ui/Loading';
+import { useQuery } from '@/hooks/useFetch';
+import AuthorInfo, {
+ AUTHOR_INFO_VARIANTS,
+} from '@/pages/Product/components/AuthorInfo';
+import CommentForm from '@/pages/Product/components/CommentForm';
+import DropdownMenu from '@/pages/Product/components/DropdownMenu';
+import { getComments } from '@/pages/Product/lib/api';
+import styles from '@/pages/Product/styles/CommentSection.module.scss';
+import { getTimeDiffrenceString } from '@/utils/Date';
+
+export default function CommentSection() {
+ const { productId } = useParams();
+ const { data, loading } = useQuery({
+ queryFn: () => getComments({ productId }),
+ });
+ if (!data || loading) return
;
+ const isEmptyComment = data.list.length === 0;
+ return (
+ <>
+
+
+ {isEmptyComment && (
+
+

+
아직 문의가 없어요
+
+ )}
+ {data.list.map((comment) => (
+
+
+
+ ))}
+
+ >
+ );
+}
diff --git a/src/pages/Product/components/DropdownMenu.jsx b/src/pages/Product/components/DropdownMenu.jsx
new file mode 100644
index 00000000..c674269f
--- /dev/null
+++ b/src/pages/Product/components/DropdownMenu.jsx
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+
+import ThreeDotIcon from '@/assets/icons/ic_kebab.svg';
+import styles from '@/pages/Product/styles/DropdownMenu.module.scss';
+
+export default function DropdownMenu({ menuNameArray }) {
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+ const handleClick = () => {
+ setIsDropdownOpen((prev) => !prev);
+ };
+ return (
+
+
+ {isDropdownOpen && (
+
+ {menuNameArray.map((menuName) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/pages/Product/components/InfoSection.jsx b/src/pages/Product/components/InfoSection.jsx
new file mode 100644
index 00000000..6fd9e882
--- /dev/null
+++ b/src/pages/Product/components/InfoSection.jsx
@@ -0,0 +1,66 @@
+import { useParams } from 'react-router-dom';
+
+import LikeIcon from '@/assets/icons/ic_heart.svg';
+import KebabIcon from '@/assets/icons/ic_kebab.svg';
+import ItemImg from '@/components/ui/ItemImg';
+import Loading from '@/components/ui/Loading';
+import Tag from '@/components/ui/Tag';
+import { useQuery } from '@/hooks/useFetch';
+import AuthorInfo, {
+ AUTHOR_INFO_VARIANTS,
+} from '@/pages/Product/components/AuthorInfo';
+import { getProduct } from '@/pages/Product/lib/api';
+import styles from '@/pages/Product/styles/InfoSection.module.scss';
+import { getFormattedDate } from '@/utils/Date';
+
+export default function InfoSection() {
+ const { productId } = useParams();
+ const { data, loading } = useQuery({
+ queryFn: () => getProduct({ productId }),
+ });
+ if (!data || loading) return
;
+ const formattedDate = getFormattedDate(data?.createdAt);
+ return (
+
+
+
+
+
+
+
+ {data.name}
+
+ {data.price.toLocaleString('ko-KR') + '원'}
+
+
+
+
+
상품 소개
+
{data.description}
+
상품 태그
+
+ {data.tags.map((tag) => (
+ {tag}
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/Product/index.jsx b/src/pages/Product/index.jsx
new file mode 100644
index 00000000..76f215df
--- /dev/null
+++ b/src/pages/Product/index.jsx
@@ -0,0 +1,20 @@
+import BackIcon from '@/assets/icons/ic_back.svg';
+import Button from '@/components/ui/Button';
+import CommentSection from '@/pages/Product/components/CommentSection';
+import InfoSection from '@/pages/Product/components/InfoSection';
+import styles from '@/pages/Product/styles/index.module.scss';
+
+export default function Product() {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/Product/lib/api.js b/src/pages/Product/lib/api.js
new file mode 100644
index 00000000..5c0e83c7
--- /dev/null
+++ b/src/pages/Product/lib/api.js
@@ -0,0 +1,13 @@
+import { BASE_API_URL } from '@/apis/constants';
+import { customFetch } from '@/apis/customFetch';
+
+export const getProduct = async ({ productId }) => {
+ const data = await customFetch(`${BASE_API_URL}/products/${productId}`);
+ return data;
+};
+export const getComments = async ({ productId, limit = 8, cursor = '' }) => {
+ const data = await customFetch(
+ `${BASE_API_URL}/products/${productId}/comments?limit=${limit}&curosr=${cursor}`
+ );
+ return data;
+};
diff --git a/src/pages/Product/lib/util.js b/src/pages/Product/lib/util.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/pages/Product/styles/AuthorInfo.module.scss b/src/pages/Product/styles/AuthorInfo.module.scss
new file mode 100644
index 00000000..f419a136
--- /dev/null
+++ b/src/pages/Product/styles/AuthorInfo.module.scss
@@ -0,0 +1,44 @@
+.authorInfo {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-md);
+
+ .profileImg {
+ background-color: var(--color-gray-300);
+ border-radius: var(--radius-circle);
+ &Product {
+ width: 2.5rem;
+ height: 2.5rem;
+ }
+ &Comment {
+ width: 2rem;
+ height: 2rem;
+ }
+ &Community {
+ width: 1.5rem;
+ height: 1.5rem;
+ }
+ }
+ .authorWrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ font-size: var(--font-size-sm);
+ gap: var(--spacing-xs);
+ &Comment {
+ font-size: var(--font-size-xs);
+ }
+ &Community {
+ flex-direction: row;
+ gap: 0.5rem;
+ }
+ .nickname {
+ color: var(--color-gray-600);
+ font-weight: 400;
+ }
+ .updateAt {
+ color: var(--color-gray-400);
+ font-weight: 500;
+ }
+ }
+}
diff --git a/src/pages/Product/styles/CommentForm.module.scss b/src/pages/Product/styles/CommentForm.module.scss
new file mode 100644
index 00000000..8cc00420
--- /dev/null
+++ b/src/pages/Product/styles/CommentForm.module.scss
@@ -0,0 +1,30 @@
+.form {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ gap: rem(9px);
+
+ .title {
+ font-size: var(--font-size-md);
+ font-weight: 600;
+ }
+ .commentInput {
+ height: rem(130px);
+ border-radius: var(--radius-sm);
+ background-color: var(--color-gray-100);
+ padding: var(--spacing-lg) var(--spacing-lg);
+ resize: none;
+ &::placeholder {
+ color: var(--color-gray-400);
+ text-wrap: wrap;
+ }
+ @include tablet {
+ height: rem(104px);
+ }
+ }
+ .buttonWrapper {
+ align-self: flex-end;
+ width: rem(74px);
+ height: rem(42px);
+ }
+}
diff --git a/src/pages/Product/styles/CommentSection.module.scss b/src/pages/Product/styles/CommentSection.module.scss
new file mode 100644
index 00000000..954e716d
--- /dev/null
+++ b/src/pages/Product/styles/CommentSection.module.scss
@@ -0,0 +1,40 @@
+.section {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ gap: var(--spacing-lg);
+ .emptyComment {
+ @include flex-center;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+ img {
+ width: rem(140px);
+ height: rem(140px);
+
+ @include tablet {
+ width: rem(196px);
+ height: rem(196px);
+ }
+ }
+ span {
+ color: var(--color-gray-400);
+ font-weight: 300;
+ }
+ }
+ .commentContainer {
+ display: flex;
+ flex-direction: column;
+ padding-bottom: rem(12px);
+ border-bottom: 1px solid var(--color-gray-300);
+ gap: var(--spacing-lg);
+ .commentWrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ .comment {
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ }
+ }
+ }
+}
diff --git a/src/pages/Product/styles/DropdownMenu.module.scss b/src/pages/Product/styles/DropdownMenu.module.scss
new file mode 100644
index 00000000..9255f4bc
--- /dev/null
+++ b/src/pages/Product/styles/DropdownMenu.module.scss
@@ -0,0 +1,36 @@
+.container {
+ font-weight: 400;
+ font-size: var(--font-size-sm);
+ position: relative;
+ .menuButton {
+ background: none;
+ }
+ .options {
+ @include flex-center;
+ flex-direction: column;
+ position: absolute;
+ top: rem(28px);
+ right: 0px;
+
+ & > button:first-child {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ & > button:last-child {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+ .option {
+ background-color: var(--color-white);
+ border: 1px solid var(--color-gray-200);
+ border-radius: var(--radius-sm);
+ padding: var(--spacing-sm) var(--spacing-lg);
+ width: 8rem;
+ &:hover {
+ background-color: var(--color-primary);
+ border: 1px solid var(--color-primary);
+ color: var(--color-white);
+ }
+ }
+ }
+}
diff --git a/src/pages/Product/styles/InfoSection.module.scss b/src/pages/Product/styles/InfoSection.module.scss
new file mode 100644
index 00000000..c4aaf2e9
--- /dev/null
+++ b/src/pages/Product/styles/InfoSection.module.scss
@@ -0,0 +1,137 @@
+.section {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: var(--spacing-md);
+ width: 100%;
+ @include tablet {
+ flex-direction: row;
+ gap: var(--spacing-lg);
+ }
+ .imgWrapper {
+ min-width: 100%;
+ max-width: 30.375rem;
+ @include tablet {
+ min-width: 50%;
+ max-width: 50%;
+ }
+ @include desktop {
+ min-width: 30.375rem;
+ }
+ }
+ .contentsContainer {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ gap: rem(40px);
+ @include tablet {
+ gap: var(--spacing-3xl);
+ }
+ }
+ .texts {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-md);
+ @include desktop {
+ gap: var(--spacing-lg);
+ }
+ .titles {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-md);
+ padding-bottom: var(--spacing-md);
+ border-bottom: 1px solid var(--color-gray-200);
+ .title {
+ font-size: var(--font-size-md);
+ font-weight: 600;
+ @include tablet {
+ font-size: var(--font-size-xl);
+ }
+ @include desktop {
+ font-size: var(--font-size-2xl);
+ }
+ }
+ .price {
+ font-size: var(--font-size-2xl);
+ font-weight: 600;
+ @include tablet {
+ font-size: var(--font-size-3xl);
+ }
+ @include desktop {
+ font-size: rem(40px);
+ }
+ }
+ svg {
+ position: absolute;
+ right: 1rem;
+ top: 0;
+ }
+ }
+ .infoContainer {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-lg);
+ .infoLabel {
+ font-weight: 600;
+ font-size: var(--font-size-sm);
+ @include desktop {
+ font-size: var(--font-size-md);
+ }
+ }
+ .description {
+ color: var(--color-gray-800);
+ font-weight: 500;
+ }
+ .tags {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ }
+ }
+ }
+ .sectionFooter {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ .likeButtonWrapper {
+ padding-left: 1.5rem;
+ border-left: 1px solid var(--color-gray-200);
+ .likeButton {
+ @include flex-center;
+ gap: var(--spacing-xs);
+ background-color: var(--color-white);
+ border: 1px solid var(--color-gray-200);
+ border-radius: rem(35px);
+ padding: rem(4px) rem(12px);
+ font-weight: 500;
+ transition: border 0.3s ease-in;
+ &:hover {
+ border: 1px solid var(--color-primary);
+ svg {
+ path {
+ stroke: var(--color-primary);
+ }
+ }
+ }
+ &:active {
+ svg {
+ fill: var(--color-primary);
+ }
+ }
+ svg {
+ width: 1.5rem;
+ height: 1.5rem;
+ path {
+ transition: stroke 0.3s ease-in;
+ stroke: var(--color-gray-500);
+ }
+ }
+ .count {
+ color: var(--color-gray-500);
+ }
+ }
+ }
+ }
+}
diff --git a/src/pages/Product/styles/index.module.scss b/src/pages/Product/styles/index.module.scss
new file mode 100644
index 00000000..d2a20b0d
--- /dev/null
+++ b/src/pages/Product/styles/index.module.scss
@@ -0,0 +1,30 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--spacing-2xl);
+ padding: var(--spacing-md) var(--spacing-md);
+ @include desktop {
+ padding: var(--spacing-2xl) var(--spacing-xl);
+ }
+
+ @include tablet {
+ width: 100%;
+ }
+ @include desktop {
+ margin-left: auto;
+ margin-right: auto;
+ padding: var(--spacing-2xl) 0;
+ max-width: rem(1200px);
+ }
+ .buttonWrapper {
+ width: rem(240px);
+ a {
+ font-size: var(--font-size-lg);
+ font-weight: 600;
+ border-radius: var(--radius-lg);
+ gap: var(--spacing-sm);
+ }
+ }
+}
diff --git a/src/pages/Signup.jsx b/src/pages/Signup/index.jsx
similarity index 100%
rename from src/pages/Signup.jsx
rename to src/pages/Signup/index.jsx
diff --git a/src/styles/_font.scss b/src/styles/_font.scss
new file mode 100644
index 00000000..26857998
--- /dev/null
+++ b/src/styles/_font.scss
@@ -0,0 +1,61 @@
+@mixin font-face($name, $path, $weight: null, $format: null) {
+ @font-face {
+ font-family: $name;
+ font-weight: $weight;
+ src: url($path) format($format);
+ }
+}
+@include font-face(
+ 'ROKAF_Sans_Medium',
+ '@/assets/fonts/ROKAF_Sans/ROKAF_Sans_Medium.ttf',
+ 400,
+ 'truetype'
+);
+@include font-face(
+ 'pretendard',
+ '@/assets/fonts/Pretendard/Pretendard-Thin.woff2',
+ 100,
+ 'woff2'
+);
+@include font-face(
+ 'pretendard',
+ '@/assets/fonts/Pretendard/Pretendard-ExtraLight.woff2',
+ 200,
+ 'woff2'
+);
+@include font-face(
+ 'pretendard',
+ '@/assets/fonts/Pretendard/Pretendard-Light.woff2',
+ 300,
+ 'woff2'
+);
+@include font-face(
+ 'pretendard',
+ '@/assets/fonts/Pretendard/Pretendard-Medium.woff2',
+ 400,
+ 'woff2'
+);
+@include font-face(
+ 'pretendard',
+ '@/assets/fonts/Pretendard/Pretendard-Regular.woff2',
+ 500,
+ 'woff2'
+);
+@include font-face(
+ 'pretendard',
+ '@/assets/fonts/Pretendard/Pretendard-SemiBold.woff2',
+ 600,
+ 'woff2'
+);
+@include font-face(
+ 'pretendard',
+ '@/assets/fonts/Pretendard/Pretendard-Bold.woff2',
+ 700,
+ 'woff2'
+);
+@include font-face(
+ 'pretendard',
+ '@/assets/fonts/Pretendard/Pretendard-ExtraBold.woff2',
+ 800,
+ 'woff2'
+);
diff --git a/src/styles/_media.scss b/src/styles/_media.scss
new file mode 100644
index 00000000..d9822759
--- /dev/null
+++ b/src/styles/_media.scss
@@ -0,0 +1,13 @@
+$breakpoint-desktop: 1280px;
+$breakpoint-tablet: 768px;
+
+@mixin desktop {
+ @media (min-width: #{$breakpoint-desktop}) {
+ @content;
+ }
+}
+@mixin tablet {
+ @media (min-width: #{$breakpoint-tablet}) {
+ @content;
+ }
+}
diff --git a/src/styles/_mixin.scss b/src/styles/_mixin.scss
new file mode 100644
index 00000000..36f927c4
--- /dev/null
+++ b/src/styles/_mixin.scss
@@ -0,0 +1,5 @@
+@mixin flex-center {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
diff --git a/src/styles/_util.scss b/src/styles/_util.scss
new file mode 100644
index 00000000..ed352a82
--- /dev/null
+++ b/src/styles/_util.scss
@@ -0,0 +1,10 @@
+@use 'sass:math';
+
+/* px to rem */
+$base-rem-size: 16px;
+@function remove-unit($value) {
+ @return math.div($value, ($value * 0 + 1));
+}
+@function rem($px, $base: $base-rem-size) {
+ @return (remove-unit(math.div($px, $base))) * 1rem;
+}
diff --git a/src/styles/fonts.css b/src/styles/fonts.css
deleted file mode 100644
index f9064991..00000000
--- a/src/styles/fonts.css
+++ /dev/null
@@ -1,48 +0,0 @@
-@font-face {
- font-family: "ROKAF_Sans_Medium";
- font-weight: 400;
- src: url("@/assets/fonts/ROKAF_Sans/ROKAF_Sans_Medium.ttf") format("truetype");
-}
-@font-face {
- font-family: "pretendard";
- font-weight: 800;
- src: url("@/assets/fonts/Pretendard/Pretendard-ExtraBold.woff2")
- format("woff2");
-}
-@font-face {
- font-family: "pretendard";
- font-weight: 700;
- src: url("@/assets/fonts/Pretendard/Pretendard-Bold.woff2") format("woff2");
-}
-@font-face {
- font-family: "pretendard";
- font-weight: 600;
- src: url("@/assets/fonts/Pretendard/Pretendard-SemiBold.woff2")
- format("woff2");
-}
-@font-face {
- font-family: "pretendard";
- font-weight: 500;
- src: url("@/assets/fonts/Pretendard/Pretendard-Regular.woff2") format("woff2");
-}
-@font-face {
- font-family: "pretendard";
- font-weight: 400;
- src: url("@/assets/fonts/Pretendard/Pretendard-Medium.woff2") format("woff2");
-}
-@font-face {
- font-family: "pretendard";
- font-weight: 300;
- src: url("@/assets/fonts/Pretendard/Pretendard-Light.woff2") format("woff2");
-}
-@font-face {
- font-family: "pretendard";
- font-weight: 200;
- src: url("@/assets/fonts/Pretendard/Pretendard-ExtraLight.woff2")
- format("woff2");
-}
-@font-face {
- font-weight: "pretendard";
- font-family: 100;
- src: url("@/assets/fonts/Pretendard/Pretendard-Thin.woff2") format("woff2");
-}
diff --git a/src/styles/global.js b/src/styles/global.js
deleted file mode 100644
index 43e4d547..00000000
--- a/src/styles/global.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { createGlobalStyle } from 'styled-components';
-
-import '@/styles/fonts.css';
-
-export const GlobalStyle = createGlobalStyle`
- body {
- font-family: "pretendard", sans-serif;
- }
- * {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
- border: none;
- font-size: inherit;
- font-weight: inherit;
- }
- a {
- text-decoration: none;
- color: inherit;
- }
- button {
- font-family: inherit;
- cursor: pointer;
- }
- button:disabled {
- cursor: not-allowed;
- }
- li {
- list-style: none;
- }
- textarea{
- font-family: "pretendard", sans-serif;
- font-size: 1rem;
- }
-`;
diff --git a/src/styles/global.scss b/src/styles/global.scss
new file mode 100644
index 00000000..64028154
--- /dev/null
+++ b/src/styles/global.scss
@@ -0,0 +1,76 @@
+:root {
+ --color-primary: #3692ff;
+ --color-gray-900: #111827;
+ --color-gray-800: #1f2937;
+ --color-gray-700: #374151;
+ --color-gray-600: #4b5563;
+ --color-gray-500: #6b7280;
+ --color-gray-400: #9ca3af;
+ --color-gray-300: #d1d5db;
+ --color-gray-200: #e5e7eb;
+ --color-gray-100: #f3f4f6;
+ --color-gray-50: #f9fafb;
+ --color-white: #ffffff;
+ --color-error-red: #f74747;
+
+ /* Typography */
+ --font-primary: 'pretendard', sans-serif;
+ --font-secondary: 'ROKAF_Sans_Medium', sans-serif;
+
+ --font-size-xs: 0.75rem; /* 12px */
+ --font-size-sm: 0.875rem; /* 14px */
+ --font-size-md: 1rem; /* 16px, base */
+ --font-size-lg: 1.125rem; /* 18px */
+ --font-size-xl: 1.25rem; /* 20px */
+ --font-size-2xl: 1.5rem; /* 24px */
+ --font-size-3xl: 1.75rem; /* 28px */
+
+ /* spacing */
+ --spacing-xs: 0.3125rem;
+ --spacing-sm: 0.625rem;
+ --spacing-md: 1rem;
+ --spacing-lg: 1.5rem;
+ --spacing-xl: 2rem;
+ --spacing-2xl: 3rem;
+ --spacing-3xl: 4rem;
+ --spacing-4xl: 8rem;
+ --spacing-header: 4.5rem;
+
+ /* border-radius */
+ --radius-xs: 0.5rem;
+ --radius-sm: 0.75rem;
+ --radius-md: 1.25rem;
+ --radius-lg: 2.5rem;
+ --radius-circle: 9999px;
+
+ --width-container: 75rem;
+}
+body {
+ font-family: 'pretendard', sans-serif;
+}
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ border: none;
+ font-size: inherit;
+ font-weight: inherit;
+}
+a {
+ text-decoration: none;
+ color: inherit;
+}
+button {
+ font-family: inherit;
+ cursor: pointer;
+}
+button:disabled {
+ cursor: not-allowed;
+}
+li {
+ list-style: none;
+}
+textarea {
+ font-family: 'pretendard', sans-serif;
+ font-size: 1rem;
+}
diff --git a/src/styles/index.scss b/src/styles/index.scss
new file mode 100644
index 00000000..2f171baf
--- /dev/null
+++ b/src/styles/index.scss
@@ -0,0 +1,4 @@
+@forward 'font.scss';
+@forward 'media.scss';
+@forward 'mixin.scss';
+@forward 'util.scss';
diff --git a/src/styles/media.js b/src/styles/media.js
deleted file mode 100644
index 841992b9..00000000
--- a/src/styles/media.js
+++ /dev/null
@@ -1,12 +0,0 @@
-const screenSize = {
- TABLET: '48rem',
- DESKTOP: '80rem',
-};
-export const screenSizeNumber = {
- TABLET: 768,
- DESKTOP: 1280,
-};
-export const device = {
- TABLET: `(min-width: ${screenSize.TABLET})`,
- DESKTOP: `(min-width: ${screenSize.DESKTOP})`,
-};
diff --git a/src/styles/theme.ts b/src/styles/theme.ts
deleted file mode 100644
index 11e05dfc..00000000
--- a/src/styles/theme.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-const colors = {
- primary: '#3692ff',
- gray800: '#1f2937',
- gray700: '#374151',
- gray600: '#4b5563',
- gray500: '#6b7280',
- gray400: '#9ca3af',
- gray200: '#e5e7eb',
- gray100: '#f3f4f6',
- gray50: '#F9FAFB',
- errorRed: '#f74747',
- white: '#ffffff',
-};
-
-const fontFamily = {
- primary: `"pretendard", sans-serif`,
- logo: `"ROKAF_Sans_Medium", sans-serif`,
-};
-
-const fontSize = {
- xs: '0.75rem' /* 12px */,
- sm: '0.875rem' /* 14px */,
- md: '1rem' /* 16px, base */,
- lg: '1.125rem' /* 18px */,
- xl: '1.25rem' /* 20px,*/,
- '2xl': '1.5rem' /* 24px */,
- '3xl': '1.75rem' /* 28px */,
-};
-
-const spacing = {
- header: '60px',
- xs: '5px',
- sm: '10px',
- md: '16px',
- lg: '24px',
- xl: '32px',
- '2xl': '48px',
- '3xl': '64px',
- '4xl': '128px',
-};
-
-const borderRadius = {
- xs: '8px',
- sm: '12px',
- md: '16px',
- lg: '20px',
- xl: '40px',
- circle: '9999px',
-};
-
-const theme = {
- colors,
- fontFamily,
- fontSize,
- spacing,
- borderRadius,
-};
-
-export default theme;
-export type Theme = typeof theme;
diff --git a/src/styles/util.js b/src/styles/util.js
new file mode 100644
index 00000000..3e226b9d
--- /dev/null
+++ b/src/styles/util.js
@@ -0,0 +1,7 @@
+import { css } from 'styled-components';
+
+export const flexCenter = css`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
diff --git a/src/types/styled.d.ts b/src/types/styled.d.ts
deleted file mode 100644
index 7401829c..00000000
--- a/src/types/styled.d.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { CSSProp } from 'styled-components';
-import type { Theme } from '@/styles/theme';
-
-declare module 'styled-components' {
- export interface DefaultTheme extends Theme {
- /*
- * 필요하다면 여기서 타입을 정의해줘도 됨
- * 하지만 "Theme"가 수정될 때마다 수정사항을 반영해줘야 하기 때문에 "extends Theme"형태로 적는 것이 좋음
- */
- }
-}
diff --git a/src/utils/Date.js b/src/utils/Date.js
new file mode 100644
index 00000000..a76ec3e1
--- /dev/null
+++ b/src/utils/Date.js
@@ -0,0 +1,35 @@
+function isValidDate(date) {
+ return date instanceof Date && !isNaN(date.getTime());
+}
+export function getFormattedDate(date) {
+ const newDate = new Date(date);
+ if (!isValidDate(newDate)) return '';
+
+ const krDate = newDate.toLocaleString('ko-KR');
+ const formattedDate = krDate.slice(0, krDate.lastIndexOf('.'));
+ return formattedDate;
+}
+export const getTimeDiffrenceString = (date) => {
+ const today = new Date();
+ const dateValue = new Date(date);
+ if (!isValidDate(dateValue)) return '';
+
+ const diffrence = Math.floor(
+ (today.getTime() - dateValue.getTime()) / 1000 / 60
+ );
+ if (diffrence < 60) {
+ return `${diffrence}분전`;
+ }
+
+ const hourDifference = Math.floor(diffrence / 60);
+ if (hourDifference < 24) {
+ return `${hourDifference}시간전`;
+ }
+
+ const dayDifference = Math.floor(diffrence / 60 / 24);
+ if (dayDifference < 365) {
+ return `${dayDifference}일전`;
+ }
+
+ return `${Math.floor(dayDifference / 365)}년전`;
+};
diff --git a/vite.config.js b/vite.config.js
index 6303efc8..d5ae7346 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -18,7 +18,13 @@ export default defineConfig(({mode}) => ({
include: "**/*.svg",
}),
],
-
+ css: {
+ preprocessorOptions: {
+ scss: {
+ additionalData: `@use '@/styles' as *;`,
+ },
+ },
+ },
esbuild: {
//build에 console, debugger 제거
drop: mode === 'production'? ["debugger", "console"] : [],