diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 4dcb439..d94d95f 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -16,5 +16,7 @@ module.exports = {
'warn',
{ allowConstantExport: true },
],
+ 'react/jsx-props-no-spreading': 'off',
+ 'react/prop-types': 'off',
},
-}
+};
diff --git a/package-lock.json b/package-lock.json
index 587ad9f..2034cde 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,10 +12,13 @@
"axios": "^1.5.0",
"chart.js": "^4.4.0",
"react": "^18.2.0",
+ "react-chartjs-2": "^5.2.0",
+ "react-cookie": "^6.1.1",
"react-dom": "^18.2.0",
"react-query": "^3.39.3",
"react-router-dom": "^6.16.0",
- "styled-components": "^6.0.8"
+ "styled-components": "^6.0.8",
+ "zustand": "^4.4.1"
},
"devDependencies": {
"@types/react": "^18.2.15",
@@ -2241,6 +2244,17 @@
"integrity": "sha512-xVRaR4u9hcYjFvcSg71Lz5Bo4//CyjAAfMxa7UsaDSYxAshflUkVJWiyVWrfxC59z2kP1IzI4/1BEpnhI9o3Mw==",
"dev": true
},
+ "node_modules/@swc/helpers": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
+ "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@swc/types": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
@@ -2267,17 +2281,29 @@
"date-fns": "^2.25.0"
}
},
+ "node_modules/@types/cookie": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.2.tgz",
+ "integrity": "sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA=="
+ },
+ "node_modules/@types/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==",
+ "dependencies": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
"node_modules/@types/prop-types": {
"version": "15.7.7",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.7.tgz",
- "integrity": "sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog==",
- "dev": true
+ "integrity": "sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog=="
},
"node_modules/@types/react": {
"version": "18.2.22",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.22.tgz",
"integrity": "sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA==",
- "dev": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -2296,8 +2322,7 @@
"node_modules/@types/scheduler": {
"version": "0.16.4",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz",
- "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==",
- "dev": true
+ "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ=="
},
"node_modules/@types/stylis": {
"version": "4.2.1",
@@ -2821,6 +2846,14 @@
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
},
+ "node_modules/cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/core-js-compat": {
"version": "3.32.2",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.2.tgz",
@@ -3724,6 +3757,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@@ -4629,9 +4670,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.30",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz",
- "integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==",
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
@@ -4725,6 +4766,28 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-chartjs-2": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz",
+ "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react-cookie": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-6.1.1.tgz",
+ "integrity": "sha512-fuFRpf8LH6SfmVMowDUIRywJF5jAUDUWrm0EI5VdXfTl5bPcJ7B0zWbuYpT0Tvikx7Gs18MlvAT+P+744dUz2g==",
+ "dependencies": {
+ "@types/hoist-non-react-statics": "^3.3.1",
+ "hoist-non-react-statics": "^3.3.2",
+ "universal-cookie": "^6.0.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.3.0"
+ }
+ },
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@@ -4740,8 +4803,7 @@
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-query": {
"version": "3.39.3",
@@ -5454,6 +5516,15 @@
"node": ">=4"
}
},
+ "node_modules/universal-cookie": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-6.1.1.tgz",
+ "integrity": "sha512-33S9x3CpdUnnjwTNs2Fgc41WGve2tdLtvaK2kPSbZRc5pGpz2vQFbRWMxlATsxNNe/Cy8SzmnmbuBM85jpZPtA==",
+ "dependencies": {
+ "@types/cookie": "^0.5.1",
+ "cookie": "^0.5.0"
+ }
+ },
"node_modules/unload": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
@@ -5501,6 +5572,14 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/vite": {
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
@@ -5668,6 +5747,33 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zustand": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz",
+ "integrity": "sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==",
+ "dependencies": {
+ "use-sync-external-store": "1.2.0"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index 021d122..ef20bc2 100644
--- a/package.json
+++ b/package.json
@@ -14,10 +14,13 @@
"axios": "^1.5.0",
"chart.js": "^4.4.0",
"react": "^18.2.0",
+ "react-chartjs-2": "^5.2.0",
+ "react-cookie": "^6.1.1",
"react-dom": "^18.2.0",
"react-query": "^3.39.3",
"react-router-dom": "^6.16.0",
- "styled-components": "^6.0.8"
+ "styled-components": "^6.0.8",
+ "zustand": "^4.4.1"
},
"devDependencies": {
"@types/react": "^18.2.15",
diff --git a/src/API.js b/src/API.js
new file mode 100644
index 0000000..a89b32c
--- /dev/null
+++ b/src/API.js
@@ -0,0 +1,39 @@
+import axios from 'axios';
+import { Cookies } from 'react-cookie';
+
+export const API = axios.create({
+ baseURL: import.meta.env.VITE_SERVER_URL ?? '/',
+ withCredentials: true,
+ header: {
+ 'Content-Type': 'application/json',
+ cache: 'no-cache',
+ accept: 'application/json',
+ referrer: 'no-referrer',
+ },
+ xsrfCookieName: 'csrftoken',
+ xsrfHeaderName: 'x-csrftoken',
+ timeout: 1000 * 60 * 5,
+});
+
+const cookies = new Cookies();
+
+export const setCookie = (name, value, options) => {
+ return cookies.set(name, value, options);
+};
+
+export const getCookie = (name) => {
+ return cookies.get(name);
+};
+
+export const URL = {
+ login: '/user/login/',
+ logout: '/user/logout/',
+ myPage: '/user/mypage/',
+ user: '/user/',
+ brand: '/brand/',
+ product: '/product/',
+ post: '/post/',
+ write: '/write',
+ write2: '/newPost',
+ graph: '/graph/g',
+};
diff --git a/src/App.jsx b/src/App.jsx
index 5a265f7..0a93d57 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,9 +1,20 @@
import { QueryClient, QueryClientProvider } from 'react-query';
+import Router from './Router';
+import GlobalStyle from './styles/global';
+import { ThemeProvider } from 'styled-components';
+import theme from './styles/Themes.styles';
const queryClient = new QueryClient();
const App = () => {
- return ;
+ return (
+
+
+
+
+
+
+ );
};
export default App;
diff --git a/src/Router.jsx b/src/Router.jsx
new file mode 100644
index 0000000..31c5996
--- /dev/null
+++ b/src/Router.jsx
@@ -0,0 +1,80 @@
+import { BrowserRouter, Route, Routes } from 'react-router-dom';
+import PageLayout from './components/Layout/PageLayout';
+import NotFound from './pages/NotFound';
+import Products from './pages/Products';
+import ProductDetail from './pages/ProductDetail';
+import { URL } from './API';
+import Brand from './pages/Brand';
+import Posts from './pages/Posts';
+import PostDetail from './pages/PostDetail';
+import Write from './pages/Write';
+import Login from './pages/Login';
+import Logout from './pages/Logout';
+import Signup from './pages/Signup';
+import WriteSteps from './pages/WriteSteps';
+import CurrencyInput from './components/CurrencyInput';
+import { useState } from 'react';
+import MyPage from './pages/MyPage';
+
+const Router = () => (
+
+
+ }>
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+
+ }
+ />
+
+
+);
+
+export default Router;
diff --git a/src/assets/logo.svg b/src/assets/logo.svg
new file mode 100644
index 0000000..ece354c
--- /dev/null
+++ b/src/assets/logo.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/phone.svg b/src/assets/phone.svg
new file mode 100644
index 0000000..b6a4b8a
--- /dev/null
+++ b/src/assets/phone.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/Brand/BrandProductList.jsx b/src/components/Brand/BrandProductList.jsx
new file mode 100644
index 0000000..3fa360b
--- /dev/null
+++ b/src/components/Brand/BrandProductList.jsx
@@ -0,0 +1,19 @@
+import { useBrandProduct } from '../../hooks/network/brand';
+import ProductItem from '../ProductItem';
+
+const BrandProductList = ({ id }) => {
+ const { data } = useBrandProduct(id);
+
+ return (
+ <>
+ {data?.map((product) => (
+
+ ))}
+ >
+ );
+};
+
+export default BrandProductList;
diff --git a/src/components/Brand/BrandTitle.jsx b/src/components/Brand/BrandTitle.jsx
new file mode 100644
index 0000000..d800fa9
--- /dev/null
+++ b/src/components/Brand/BrandTitle.jsx
@@ -0,0 +1,15 @@
+import { Suspense } from 'react';
+import { useBrand } from '../../hooks/network/brand';
+import { Title } from '../../styles/Common.styles';
+
+const BrandTitle = ({ id }) => {
+ const { data } = useBrand(id);
+
+ return (
+
+ {data?.name}
+
+ );
+};
+
+export default BrandTitle;
diff --git a/src/components/Button/index.jsx b/src/components/Button/index.jsx
new file mode 100644
index 0000000..cf52861
--- /dev/null
+++ b/src/components/Button/index.jsx
@@ -0,0 +1,27 @@
+import * as S from '../../styles/Button.styles';
+
+function Button({
+ children,
+ onClick,
+ className,
+ disabled,
+ secondary,
+ type = 'button',
+ ...rest
+}) {
+ const ButtonComponent = secondary ? S.Button.secondary : S.Button.primary;
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default Button;
diff --git a/src/components/ButtonArea/index.jsx b/src/components/ButtonArea/index.jsx
new file mode 100644
index 0000000..a74b78a
--- /dev/null
+++ b/src/components/ButtonArea/index.jsx
@@ -0,0 +1,11 @@
+import * as S from '../../styles/ButtonArea.styles';
+
+function ButtonArea({ children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default ButtonArea;
diff --git a/src/components/CSRFToken/CSRFToken.jsx b/src/components/CSRFToken/CSRFToken.jsx
new file mode 100644
index 0000000..acf07dc
--- /dev/null
+++ b/src/components/CSRFToken/CSRFToken.jsx
@@ -0,0 +1,13 @@
+import { getCookie } from '../../API';
+
+const CSRFToken = () => {
+ return (
+
+ );
+};
+
+export default CSRFToken;
diff --git a/src/components/CheckBox/index.jsx b/src/components/CheckBox/index.jsx
new file mode 100644
index 0000000..c02e10f
--- /dev/null
+++ b/src/components/CheckBox/index.jsx
@@ -0,0 +1,21 @@
+import * as S from '../../styles/CheckBox.styles';
+
+function CheckBox({ name, id, text, checked, setChecked }) {
+ return (
+
+ setChecked(!checked)}
+ />
+
+ {name}
+ {text}
+
+
+ );
+}
+
+export default CheckBox;
diff --git a/src/components/CurrencyInput/index.jsx b/src/components/CurrencyInput/index.jsx
new file mode 100644
index 0000000..adcfcdf
--- /dev/null
+++ b/src/components/CurrencyInput/index.jsx
@@ -0,0 +1,36 @@
+import * as S from '../../styles/CurrencyInput.styles';
+
+function CurrencyInput({ value, setValue }) {
+ const getRenderValue = () => {
+ if (value === '') {
+ return '';
+ }
+ const numberValue = Number(value);
+ return `${numberValue.toLocaleString()}원`;
+ };
+ const handleOnChange = (event) => {
+ if (event.nativeEvent.inputType === 'deleteContentBackward') {
+ const newValue = value.slice(0, -1);
+ newValue === 0 ? setValue('') : setValue(newValue);
+ return;
+ }
+ const { value: newValue } = event.target;
+ const onlyNumber = newValue.replace(/[^0-9]/g, '');
+ if (onlyNumber > 9999999) {
+ return;
+ }
+ onlyNumber === '0' ? setValue('') : setValue(onlyNumber);
+ };
+
+ return (
+
+
+
+ );
+}
+
+export default CurrencyInput;
diff --git a/src/components/Graph/ProductPriceGraph.jsx b/src/components/Graph/ProductPriceGraph.jsx
new file mode 100644
index 0000000..1953093
--- /dev/null
+++ b/src/components/Graph/ProductPriceGraph.jsx
@@ -0,0 +1,86 @@
+import * as S from '../../styles/ProductPriceGraph.styles';
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ // Title,
+ Tooltip,
+ // Legend,
+} from 'chart.js';
+import { Line } from 'react-chartjs-2';
+import { useGraph } from '../../hooks/network/graph';
+import theme from '../../styles/Themes.styles';
+
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ // Title,
+ Tooltip
+);
+
+const options = {
+ maintainAspectRatio: false,
+ responsive: true,
+ scales: {
+ y: {
+ ticks: {
+ stepSize: 50000,
+ },
+ display: false,
+ },
+ },
+ plugins: {
+ tooltip: {
+ padding: 10,
+ backgroundColor: 'white',
+ titleColor: theme.colors.gray900,
+ bodyColor: theme.colors.gray900,
+ borderWidth: 1,
+ borderColor: theme.colors.primary,
+ callbacks: {
+ label: (context) => `${context.dataset.data[context.dataIndex]}원`,
+ },
+ },
+ },
+};
+
+const ProductPriceGraph = ({ id }) => {
+ const { data: graphData } = useGraph(id);
+ const primaryColor = theme.colors.primary;
+ const graphStyleData = {
+ labels: graphData?.map((data) => `${data.year}.${data.month}`),
+ datasets: [
+ {
+ label: '가격',
+ data: graphData?.map((data) => data.price),
+ backgroundColor: primaryColor,
+ borderColor: primaryColor,
+ cubicInterpolationMode: 'monotone',
+ },
+ ],
+ };
+ const minPrice = graphData?.reduce((min, data) => {
+ if (min > data.price) {
+ return data.price;
+ }
+ return min;
+ }, graphData[0]?.price);
+
+ return (
+
+
+
+
+ 최근 6개월 최저가: {minPrice}원
+
+ );
+};
+
+export default ProductPriceGraph;
diff --git a/src/components/ImageViewer/index.jsx b/src/components/ImageViewer/index.jsx
new file mode 100644
index 0000000..74734e7
--- /dev/null
+++ b/src/components/ImageViewer/index.jsx
@@ -0,0 +1,33 @@
+import { useState } from 'react';
+import * as S from '../../styles/ImageViewer.style';
+import startViewTransition from '../../methods/startViewTransition';
+import { SubTitle } from '../../styles/Common.styles';
+
+function ImageViewer({ images, title }) {
+ const [currentImage, setCurrentImage] = useState(images[0]);
+ const handleClick = (image) => () => {
+ startViewTransition(() => setCurrentImage(image));
+ };
+
+ return (
+
+ {title && {title}}
+
+
+ {images.map((image) => (
+
+ ))}
+
+
+ );
+}
+
+export default ImageViewer;
diff --git a/src/components/Layout/AuthMenu.jsx b/src/components/Layout/AuthMenu.jsx
new file mode 100644
index 0000000..4bd5df5
--- /dev/null
+++ b/src/components/Layout/AuthMenu.jsx
@@ -0,0 +1,24 @@
+import * as S from '../../styles/Layout.styles';
+import { URL } from '../../API';
+import useIsLogged from '../../hooks/network/isLogged';
+
+const AuthMenu = () => {
+ const isLogged = useIsLogged((state) => state.isLogged);
+
+ if (!isLogged) {
+ return (
+
+ 로그인
+
+ );
+ }
+
+ return (
+
+ 글쓰기
+ 마이페이지
+
+ );
+};
+
+export default AuthMenu;
diff --git a/src/components/Layout/Footer.jsx b/src/components/Layout/Footer.jsx
new file mode 100644
index 0000000..3693c75
--- /dev/null
+++ b/src/components/Layout/Footer.jsx
@@ -0,0 +1,13 @@
+import * as S from '../../styles/Layout.styles';
+
+const Footer = () => {
+ return (
+
+
+ © {new Date().getFullYear()} App. All rights reserved.
+
+
+ );
+};
+
+export default Footer;
diff --git a/src/components/Layout/Header.jsx b/src/components/Layout/Header.jsx
new file mode 100644
index 0000000..e7be5ec
--- /dev/null
+++ b/src/components/Layout/Header.jsx
@@ -0,0 +1,28 @@
+import * as S from '../../styles/Layout.styles';
+import { URL } from '../../API';
+import LogoImage from '../../assets/logo.svg';
+import AuthMenu from './AuthMenu';
+
+function Header() {
+ return (
+
+
+
+
+
+
+
+ 기기 목록
+ 최신글
+
+
+
+
+
+ );
+}
+
+export default Header;
diff --git a/src/components/Layout/HeaderLinks.jsx b/src/components/Layout/HeaderLinks.jsx
new file mode 100644
index 0000000..9aa10d1
--- /dev/null
+++ b/src/components/Layout/HeaderLinks.jsx
@@ -0,0 +1,22 @@
+import * as S from '../../styles/Layout.styles';
+import { URL } from '../../API';
+import { useBrands } from '../../hooks/network/brand';
+
+const HeaderLinks = () => {
+ const { data } = useBrands();
+
+ return (
+ <>
+ {data?.map((brand) => (
+
+ {brand?.name}
+
+ ))}
+ >
+ );
+};
+
+export default HeaderLinks;
diff --git a/src/components/Layout/Nav.jsx b/src/components/Layout/Nav.jsx
new file mode 100644
index 0000000..de17769
--- /dev/null
+++ b/src/components/Layout/Nav.jsx
@@ -0,0 +1,5 @@
+function Nav() {
+ return ( );
+}
+
+export default Nav;
diff --git a/src/components/Layout/PageLayout.jsx b/src/components/Layout/PageLayout.jsx
new file mode 100644
index 0000000..0d3fcba
--- /dev/null
+++ b/src/components/Layout/PageLayout.jsx
@@ -0,0 +1,24 @@
+import * as S from '../../styles/Layout.styles';
+import { Outlet } from 'react-router';
+import Header from './Header';
+import Footer from './Footer';
+import { ErrorBoundary } from '@toss/error-boundary';
+import { Suspense } from 'react';
+
+const PageLayout = () => {
+ return (
+ <>
+
+
+ <>>}>
+ >}>
+
+
+
+
+
+ >
+ );
+};
+
+export default PageLayout;
diff --git a/src/components/Loading/index.jsx b/src/components/Loading/index.jsx
new file mode 100644
index 0000000..397f9d6
--- /dev/null
+++ b/src/components/Loading/index.jsx
@@ -0,0 +1,10 @@
+const Loading = () => {
+ return (
+
+ );
+};
+
+export default Loading;
diff --git a/src/components/Login/LoginForm.jsx b/src/components/Login/LoginForm.jsx
new file mode 100644
index 0000000..f5cf787
--- /dev/null
+++ b/src/components/Login/LoginForm.jsx
@@ -0,0 +1,72 @@
+import { API, URL, getCookie } from '../../API';
+import * as S from '../../styles/Form.styles';
+import { useState } from 'react';
+import CSRFToken from '../CSRFToken/CSRFToken';
+import { Navigate, useNavigate } from 'react-router-dom';
+import useIsLogged from '../../hooks/network/isLogged';
+
+const LoginForm = () => {
+ const navigate = useNavigate();
+ const setIsLogged = useIsLogged((state) => state.setIsLogged);
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const onSubmit = (e) => {
+ e.preventDefault();
+ API.post(URL.login, {
+ username,
+ password,
+ })
+ .then((res) => {
+ if (res.status === 200) {
+ setIsLogged(true);
+ navigate('/');
+ }
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ };
+
+ return (
+
+
+
+
아이디
+
setUsername(e.target.value)}
+ />
+
+
+
비밀번호
+
setPassword(e.target.value)}
+ />
+
+
+ navigate('/signup')}
+ />
+
+
+
+ );
+};
+
+export default LoginForm;
diff --git a/src/components/PanelLayout/index.jsx b/src/components/PanelLayout/index.jsx
new file mode 100644
index 0000000..cf8ee68
--- /dev/null
+++ b/src/components/PanelLayout/index.jsx
@@ -0,0 +1,15 @@
+import * as S from '../../styles/PanelLayout.styles';
+
+function PanelLayout({ reverse = false, children }) {
+ return {children};
+}
+
+export function LeftPanel({ children }) {
+ return {children};
+}
+
+export function RightPanel({ children }) {
+ return {children};
+}
+
+export default PanelLayout;
diff --git a/src/components/Post/PostList.jsx b/src/components/Post/PostList.jsx
new file mode 100644
index 0000000..0c704d3
--- /dev/null
+++ b/src/components/Post/PostList.jsx
@@ -0,0 +1,19 @@
+import { usePosts } from '../../hooks/network/post';
+import PostItem from '../PostItem';
+
+const PostList = ({ page }) => {
+ const { data } = usePosts({ page });
+
+ return (
+ <>
+ {data?.results?.map((post) => (
+
+ ))}
+ >
+ );
+};
+
+export default PostList;
diff --git a/src/components/PostItem/index.jsx b/src/components/PostItem/index.jsx
new file mode 100644
index 0000000..48b6249
--- /dev/null
+++ b/src/components/PostItem/index.jsx
@@ -0,0 +1,25 @@
+import PhoneImage from '../../assets/phone.svg';
+import { URL } from '../../API';
+import * as S from '../../styles/PostItem.styles';
+
+const PostItem = ({ post }) => {
+ const image = post?.image || PhoneImage;
+
+ return (
+
+
+
+
+
+ {post?.product?.name}
+ {post?.written_at}
+ {post?.price}원
+
+
+ );
+};
+
+export default PostItem;
diff --git a/src/components/ProductItem/index.jsx b/src/components/ProductItem/index.jsx
new file mode 100644
index 0000000..a8c1b60
--- /dev/null
+++ b/src/components/ProductItem/index.jsx
@@ -0,0 +1,14 @@
+import { URL } from '../../API';
+import * as S from '../../styles/Products.styles';
+
+const ProductItem = ({ product }) => {
+ const { id, name } = product;
+
+ return (
+
+ {name}
+
+ );
+};
+
+export default ProductItem;
diff --git a/src/components/ProductList/index.jsx b/src/components/ProductList/index.jsx
new file mode 100644
index 0000000..d9b67a6
--- /dev/null
+++ b/src/components/ProductList/index.jsx
@@ -0,0 +1,19 @@
+import { useProducts } from '../../hooks/network/product';
+import ProductItem from '../ProductItem';
+
+const ProductList = () => {
+ const { data } = useProducts();
+
+ return (
+ <>
+ {data?.map((product) => (
+
+ ))}
+ >
+ );
+};
+
+export default ProductList;
diff --git a/src/components/ProductPostList/index.jsx b/src/components/ProductPostList/index.jsx
new file mode 100644
index 0000000..7d2ec17
--- /dev/null
+++ b/src/components/ProductPostList/index.jsx
@@ -0,0 +1,19 @@
+import { useProductPost } from '../../hooks/network/product';
+import PostItem from '../PostItem';
+
+const ProductPostList = ({ modelId, page }) => {
+ const { data } = useProductPost({ id: modelId, page });
+
+ return (
+ <>
+ {data?.results?.map((post) => (
+
+ ))}
+ >
+ );
+};
+
+export default ProductPostList;
diff --git a/src/components/Signup/SignupForm.jsx b/src/components/Signup/SignupForm.jsx
new file mode 100644
index 0000000..3edea56
--- /dev/null
+++ b/src/components/Signup/SignupForm.jsx
@@ -0,0 +1,89 @@
+import * as S from '../../styles/Form.styles';
+import { useState } from 'react';
+import CSRFToken from '../CSRFToken/CSRFToken';
+import { API, URL } from '../../API';
+import { useNavigate } from 'react-router-dom';
+import useIsLogged from '../../hooks/network/isLogged';
+
+const SignupForm = () => {
+ const navigate = useNavigate();
+ const setIsLogged = useIsLogged((state) => state.setIsLogged);
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [password2, setPassword2] = useState('');
+ const [nickname, setNickname] = useState('');
+ const onSubmit = (e) => {
+ e.preventDefault();
+ if (password !== password2) return;
+ API.post(URL.user, {
+ username,
+ password,
+ nickname,
+ })
+ .then((res) => {
+ if (res.status === 201) {
+ setIsLogged(true);
+ navigate('/');
+ }
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ };
+
+ return (
+
+
+
+
아이디
+
setUsername(e.target.value)}
+ />
+
+
+
비밀번호
+
setPassword(e.target.value)}
+ />
+
+
+
비밀번호 확인
+
setPassword2(e.target.value)}
+ />
+
+
+
닉네임
+
setNickname(e.target.value)}
+ />
+
+
+
+ );
+};
+
+export default SignupForm;
diff --git a/src/components/Title/index.jsx b/src/components/Title/index.jsx
new file mode 100644
index 0000000..cfcb0ef
--- /dev/null
+++ b/src/components/Title/index.jsx
@@ -0,0 +1,7 @@
+import * as S from '../../styles/Common.styles';
+
+function Title({ children }) {
+ return {children};
+}
+
+export default Title;
diff --git a/src/components/Write/BrandModelSelect.jsx b/src/components/Write/BrandModelSelect.jsx
new file mode 100644
index 0000000..5fbb4a2
--- /dev/null
+++ b/src/components/Write/BrandModelSelect.jsx
@@ -0,0 +1,56 @@
+import { Suspense, useState } from 'react';
+import BrandSelect from './BrandSelect';
+import ModelSelect from './ModelSelect';
+
+const BrandModelSelect = ({ setFormData }) => {
+ const [models, setModels] = useState([]);
+
+ return (
+ <>
+
+
+
+ }
+ >
+
+
+
+
+
+
+ }
+ >
+
+
+
+ >
+ );
+};
+
+export default BrandModelSelect;
diff --git a/src/components/Write/BrandSelect.jsx b/src/components/Write/BrandSelect.jsx
new file mode 100644
index 0000000..c3a61c6
--- /dev/null
+++ b/src/components/Write/BrandSelect.jsx
@@ -0,0 +1,41 @@
+import { useEffect, useState } from 'react';
+import { useBrands } from '../../hooks/network/brand';
+
+const BrandSelect = ({ setModels }) => {
+ const [brand, setBrand] = useState('');
+ const allModels = {};
+ const { data } = useBrands();
+ data?.map((brand) => {
+ allModels[brand?.id] = brand?.products;
+ });
+ useEffect(() => {
+ setModels(allModels[brand]);
+ }, [brand]);
+
+ return (
+
+ );
+};
+
+export default BrandSelect;
diff --git a/src/components/Write/ModelSelect.jsx b/src/components/Write/ModelSelect.jsx
new file mode 100644
index 0000000..2d7e839
--- /dev/null
+++ b/src/components/Write/ModelSelect.jsx
@@ -0,0 +1,42 @@
+import { useState } from 'react';
+
+const ModelSelect = ({ models, setFormData }) => {
+ const [model, setModel] = useState('');
+ const setModelData = (model) => {
+ setModel(model);
+ setFormData((prev) => ({
+ ...prev,
+ product: model,
+ }));
+ };
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default ModelSelect;
diff --git a/src/components/Write/PriceInput.jsx b/src/components/Write/PriceInput.jsx
new file mode 100644
index 0000000..5ac0f86
--- /dev/null
+++ b/src/components/Write/PriceInput.jsx
@@ -0,0 +1,30 @@
+import { useState } from 'react';
+
+const PriceInput = ({ setFormData }) => {
+ const [price, setPrice] = useState(0);
+ const setPriceData = (e) => {
+ const price = e.target.value;
+
+ setPrice(price);
+ setFormData((prev) => ({
+ ...prev,
+ price,
+ }));
+ };
+
+ return (
+
+ );
+};
+
+export default PriceInput;
diff --git a/src/components/Write/TextInput.jsx b/src/components/Write/TextInput.jsx
new file mode 100644
index 0000000..e99042f
--- /dev/null
+++ b/src/components/Write/TextInput.jsx
@@ -0,0 +1,47 @@
+import { useEffect, useRef, useState } from 'react';
+
+const useTextAreaSize = (textAreaRef, value) => {
+ useEffect(() => {
+ if (textAreaRef) {
+ textAreaRef.style.height = '100px';
+ const scrollHeight = textAreaRef.scrollHeight;
+ textAreaRef.style.height = scrollHeight + 'px';
+ }
+ }, [textAreaRef, value]);
+};
+
+const TextInput = ({ setFormData }) => {
+ const [text, setText] = useState('');
+ const textRef = useRef();
+ const setTextData = (text) => {
+ setText(text);
+ setFormData((prev) => ({
+ ...prev,
+ text,
+ }));
+ };
+
+ useTextAreaSize(textRef.current, text);
+
+ return (
+
+
+
+
+ );
+};
+
+export default TextInput;
diff --git a/src/components/Write/WriteForm.jsx b/src/components/Write/WriteForm.jsx
new file mode 100644
index 0000000..469b860
--- /dev/null
+++ b/src/components/Write/WriteForm.jsx
@@ -0,0 +1,45 @@
+import * as S from '../../styles/Form.styles';
+import { useState } from 'react';
+import BrandModelSelect from './BrandModelSelect';
+import PriceInput from './PriceInput';
+import TextInput from './TextInput';
+import { API, URL, getCookie } from '../../API';
+import CSRFToken from '../CSRFToken/CSRFToken';
+import { useNavigate } from 'react-router-dom';
+
+const WriteForm = () => {
+ const navigate = useNavigate();
+ const [formData, setFormData] = useState({
+ product: 0,
+ price: 0,
+ text: '',
+ });
+ const onSubmit = (e) => {
+ e.preventDefault();
+ API.post(URL.post, formData).then((res) => {
+ navigate(-1);
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default WriteForm;
diff --git a/src/constants.js b/src/constants.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/hooks/network/brand.js b/src/hooks/network/brand.js
new file mode 100644
index 0000000..25fea1a
--- /dev/null
+++ b/src/hooks/network/brand.js
@@ -0,0 +1,41 @@
+import { useQuery } from 'react-query';
+import { API, URL } from '../../API';
+
+const getBrands = async () => {
+ const { data } = await API.get(`${URL.brand}/`);
+ return data;
+};
+
+export const useBrands = () => {
+ const data = useQuery('brands', () => getBrands(), {
+ suspense: true,
+ });
+
+ return data;
+};
+
+const getBrand = async (id) => {
+ const { data } = await API.get(`${URL.brand}/${id}`);
+ return data;
+};
+
+export const useBrand = (id) => {
+ const data = useQuery(['products', id], () => getBrand(id), {
+ suspense: true,
+ });
+
+ return data;
+};
+
+const getBrandProduct = async (id) => {
+ const { data } = await API.get(`${URL.brand}/${id}/product`);
+ return data;
+};
+
+export const useBrandProduct = (id) => {
+ const data = useQuery(['brandproduct', id], () => getBrandProduct(id), {
+ suspense: true,
+ });
+
+ return data;
+};
diff --git a/src/hooks/network/graph.js b/src/hooks/network/graph.js
new file mode 100644
index 0000000..a708f99
--- /dev/null
+++ b/src/hooks/network/graph.js
@@ -0,0 +1,15 @@
+import { useQuery } from 'react-query';
+import { API, URL } from '../../API';
+
+const getGraph = async (id) => {
+ const { data } = await API.get(`${URL.graph}/${id}`);
+ return data;
+};
+
+export const useGraph = (id) => {
+ const data = useQuery(['post', id], () => getGraph(id), {
+ suspense: true,
+ });
+
+ return data;
+};
diff --git a/src/hooks/network/isLogged.js b/src/hooks/network/isLogged.js
new file mode 100644
index 0000000..a7aa96f
--- /dev/null
+++ b/src/hooks/network/isLogged.js
@@ -0,0 +1,9 @@
+import { create } from 'zustand';
+import { getCookie } from '../../API';
+
+const useIsLogged = create((set) => ({
+ isLogged: !!getCookie('sessionid'),
+ setIsLogged: (isLogged) => set({ isLogged }),
+}));
+
+export default useIsLogged;
diff --git a/src/hooks/network/post.js b/src/hooks/network/post.js
new file mode 100644
index 0000000..2886bd7
--- /dev/null
+++ b/src/hooks/network/post.js
@@ -0,0 +1,29 @@
+import { API, URL } from '../../API';
+import { useQuery } from 'react-query';
+
+const getPosts = async ({ page }) => {
+ const pageString = page ? `?page=${page}` : '';
+ const { data } = await API.get(`${URL.post}${pageString}`);
+ return data;
+};
+
+export const usePosts = ({ page }) => {
+ const data = useQuery('posts', () => getPosts({ page }), {
+ suspense: true,
+ });
+
+ return data;
+};
+
+const getPost = async (id) => {
+ const { data } = await API.get(`${URL.post}${id}`);
+ return data;
+};
+
+export const usePost = (id) => {
+ const data = useQuery(['post', id], () => getPost(id), {
+ suspense: true,
+ });
+
+ return data;
+};
diff --git a/src/hooks/network/product.js b/src/hooks/network/product.js
new file mode 100644
index 0000000..55bca02
--- /dev/null
+++ b/src/hooks/network/product.js
@@ -0,0 +1,46 @@
+import { API, URL } from '../../API';
+import { useQuery } from 'react-query';
+
+const getProducts = async () => {
+ const { data } = await API.get(URL.product);
+ return data;
+};
+
+export const useProducts = () => {
+ const data = useQuery('products', () => getProducts(), {
+ suspense: true,
+ });
+
+ return data;
+};
+
+const getProduct = async (id) => {
+ const { data } = await API.get(`${URL.product}${id}`);
+ return data;
+};
+
+export const useProduct = (id) => {
+ const data = useQuery(['product', id], () => getProduct(id), {
+ suspense: true,
+ });
+
+ return data;
+};
+
+const getProductPost = async ({ id, page }) => {
+ const pageString = page ? `?page=${page}` : '';
+ const { data } = await API.get(`${URL.product}${id}/posts${pageString}`);
+ return data;
+};
+
+export const useProductPost = ({ id, page }) => {
+ const data = useQuery(
+ ['productpost', id],
+ () => getProductPost({ id, page }),
+ {
+ suspense: true,
+ }
+ );
+
+ return data;
+};
diff --git a/src/methods/startViewTransition.js b/src/methods/startViewTransition.js
new file mode 100644
index 0000000..f7625e7
--- /dev/null
+++ b/src/methods/startViewTransition.js
@@ -0,0 +1,5 @@
+function startViewTransition(callback) {
+ document.startViewTransition?.(callback) ?? callback();
+}
+
+export default startViewTransition;
diff --git a/src/pages/Brand/index.jsx b/src/pages/Brand/index.jsx
new file mode 100644
index 0000000..76aa68d
--- /dev/null
+++ b/src/pages/Brand/index.jsx
@@ -0,0 +1,16 @@
+import { useParams } from 'react-router-dom';
+import BrandProductList from '../../components/Brand/BrandProductList';
+import BrandTitle from '../../components/Brand/BrandTitle';
+
+const Brand = () => {
+ const { id } = useParams();
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default Brand;
diff --git a/src/pages/Login/index.jsx b/src/pages/Login/index.jsx
new file mode 100644
index 0000000..2c902d0
--- /dev/null
+++ b/src/pages/Login/index.jsx
@@ -0,0 +1,24 @@
+import { useNavigate } from 'react-router-dom';
+import LoginForm from '../../components/Login/LoginForm';
+import { Title } from '../../styles/Common.styles';
+import { useEffect } from 'react';
+import { getCookie } from '../../API';
+
+const Login = () => {
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (getCookie('sessionid')) {
+ navigate(-1);
+ }
+ }, []);
+
+ return (
+ <>
+ 로그인
+
+ >
+ );
+};
+
+export default Login;
diff --git a/src/pages/Logout/index.jsx b/src/pages/Logout/index.jsx
new file mode 100644
index 0000000..eb4f9dc
--- /dev/null
+++ b/src/pages/Logout/index.jsx
@@ -0,0 +1,29 @@
+import { useNavigate } from 'react-router-dom';
+import { API, URL, getCookie } from '../../API';
+import useIsLogged from '../../hooks/network/isLogged';
+import { useEffect } from 'react';
+
+const Logout = () => {
+ const navigate = useNavigate();
+ const setIsLogged = useIsLogged((state) => state.setIsLogged);
+
+ useEffect(() => {
+ API.post(URL.logout, {
+ csrftoken: getCookie('csrftoken'),
+ })
+ // API.post(URL.logout)
+ .then((res) => {
+ if (res.status === 204) {
+ setIsLogged(false);
+ }
+ navigate('/');
+ })
+ .catch((err) => {
+ console.error(err);
+ navigate('/');
+ });
+ }, []);
+ return <>>;
+};
+
+export default Logout;
diff --git a/src/pages/MyPage/index.jsx b/src/pages/MyPage/index.jsx
new file mode 100644
index 0000000..4f05ea3
--- /dev/null
+++ b/src/pages/MyPage/index.jsx
@@ -0,0 +1,26 @@
+import Title from '../../components/Title';
+import { SubTitle } from '../../styles/Common.styles';
+import * as S from '../../styles/MyPage.styles';
+
+function MyPage() {
+ return (
+ <>
+ {/* TODO: Edit Link */}
+ 마이페이지
+
+ 거래
+
+ 내가 쓴 판매글
+
+ 계정
+
+ 내가 쓴 판매글
+ 비밀번호 바꾸기
+ 회원 탈퇴
+
+
+ >
+ );
+}
+
+export default MyPage;
diff --git a/src/pages/NotFound/index.jsx b/src/pages/NotFound/index.jsx
new file mode 100644
index 0000000..42e4b91
--- /dev/null
+++ b/src/pages/NotFound/index.jsx
@@ -0,0 +1,9 @@
+const NotFound = () => {
+ return (
+ <>
+ Not Found
+ >
+ );
+};
+
+export default NotFound;
diff --git a/src/pages/PostDetail/index.jsx b/src/pages/PostDetail/index.jsx
new file mode 100644
index 0000000..334971b
--- /dev/null
+++ b/src/pages/PostDetail/index.jsx
@@ -0,0 +1,46 @@
+import * as S from '../../styles/PostDetail.styles';
+import { Link, useParams } from 'react-router-dom';
+import { usePost } from '../../hooks/network/post';
+import { URL } from '../../API';
+import ImageViewer from '../../components/ImageViewer';
+import PanelLayout, {
+ LeftPanel,
+ RightPanel,
+} from '../../components/PanelLayout';
+import ProductPriceGraph from '../../components/Graph/ProductPriceGraph';
+import { SubTitle } from '../../styles/Common.styles';
+
+const PostDetail = () => {
+ const { id } = useParams();
+ const { data } = usePost(id);
+
+ return (
+
+
+
+
+ {data.product.name}
+
+
+ {data.storage}
+ 가격 {data.price.toLocaleString()}원
+ {data.text}
+
+
+ 제품 사진
+ image.image)}
+ // title="제품 사진"
+ />
+ 최근 거래 가격
+
+
+
+
+ );
+};
+
+export default PostDetail;
diff --git a/src/pages/Posts/index.jsx b/src/pages/Posts/index.jsx
new file mode 100644
index 0000000..2bc08d6
--- /dev/null
+++ b/src/pages/Posts/index.jsx
@@ -0,0 +1,15 @@
+import * as S from '../../styles/Posts.styles';
+// import { Title } from '../../styles/Common.styles';
+import PostList from '../../components/Post/PostList';
+import Title from '../../components/Title';
+
+const Posts = () => {
+ return (
+
+ 최신글
+
+
+ );
+};
+
+export default Posts;
diff --git a/src/pages/ProductDetail/index.jsx b/src/pages/ProductDetail/index.jsx
new file mode 100644
index 0000000..50095ea
--- /dev/null
+++ b/src/pages/ProductDetail/index.jsx
@@ -0,0 +1,36 @@
+import { useParams } from 'react-router-dom';
+import { useProduct } from '../../hooks/network/product';
+import { SubTitle, Title } from '../../styles/Common.styles';
+import ProductPriceGraph from '../../components/Graph/ProductPriceGraph';
+import PanelLayout, {
+ LeftPanel,
+ RightPanel,
+} from '../../components/PanelLayout';
+import ProductPostList from '../../components/ProductPostList';
+
+const ProductDetail = () => {
+ const { id } = useParams();
+ const page = new URLSearchParams(window.location.search).get('page') ?? 1;
+ const { data } = useProduct(id);
+
+ return (
+ <>
+ {data?.name}
+
+
+ 판매글 목록
+
+
+
+ 최근 거래 가격
+
+
+
+ >
+ );
+};
+
+export default ProductDetail;
diff --git a/src/pages/Products/index.jsx b/src/pages/Products/index.jsx
new file mode 100644
index 0000000..7361212
--- /dev/null
+++ b/src/pages/Products/index.jsx
@@ -0,0 +1,13 @@
+import { Title } from '../../styles/Common.styles';
+import ProductList from '../../components/ProductList';
+
+const Products = () => {
+ return (
+ <>
+ 기기 목록
+
+ >
+ );
+};
+
+export default Products;
diff --git a/src/pages/Signup/index.jsx b/src/pages/Signup/index.jsx
new file mode 100644
index 0000000..09a3ef6
--- /dev/null
+++ b/src/pages/Signup/index.jsx
@@ -0,0 +1,13 @@
+import SignupForm from '../../components/Signup/SignupForm';
+import { Title } from '../../styles/Common.styles';
+
+const Signup = () => {
+ return (
+ <>
+ 회원가입
+
+ >
+ );
+};
+
+export default Signup;
diff --git a/src/pages/Write/index.jsx b/src/pages/Write/index.jsx
new file mode 100644
index 0000000..be68a72
--- /dev/null
+++ b/src/pages/Write/index.jsx
@@ -0,0 +1,14 @@
+import * as S from '../../styles/Form.styles';
+import WriteForm from '../../components/Write/WriteForm';
+import { Title } from '../../styles/Common.styles';
+
+const Write = () => {
+ return (
+ <>
+ 글쓰기
+
+ >
+ );
+};
+
+export default Write;
diff --git a/src/pages/WriteSteps/PhotoStep.jsx b/src/pages/WriteSteps/PhotoStep.jsx
new file mode 100644
index 0000000..59fab8b
--- /dev/null
+++ b/src/pages/WriteSteps/PhotoStep.jsx
@@ -0,0 +1,38 @@
+import Button from '../../components/Button';
+import ButtonArea from '../../components/ButtonArea';
+import ImageViewer from '../../components/ImageViewer';
+import * as S from '../../styles/WriteSteps.styles';
+
+function PhotoStep({ gotoNextStep, gotoPrevStep, photos, setPhotos }) {
+ return (
+
+ 휴대폰 사진을 올려주세요
+ 사진
+ {/* TODO: upload photo */}
+ {
+ const file = e.target.files[0];
+ const reader = new FileReader();
+ reader.onload = () => {
+ setPhotos([...photos, reader.result]);
+ };
+ reader.readAsDataURL(file);
+ }}
+ />
+ {photos && }
+
+
+
+
+
+ );
+}
+
+export default PhotoStep;
diff --git a/src/pages/WriteSteps/PriceStep.jsx b/src/pages/WriteSteps/PriceStep.jsx
new file mode 100644
index 0000000..79c4e4a
--- /dev/null
+++ b/src/pages/WriteSteps/PriceStep.jsx
@@ -0,0 +1,33 @@
+import Button from '../../components/Button';
+import ButtonArea from '../../components/ButtonArea';
+import CurrencyInput from '../../components/CurrencyInput';
+import * as S from '../../styles/WriteSteps.styles';
+
+function PriceStep({ gotoNextStep, gotoPrevStep, price, setPrice }) {
+ return (
+
+ 얼마에 팔고 싶나요?
+ 가격
+
+
+
+
+
+
+ );
+}
+
+export default PriceStep;
diff --git a/src/pages/WriteSteps/ProductSelectStep.jsx b/src/pages/WriteSteps/ProductSelectStep.jsx
new file mode 100644
index 0000000..00a4f40
--- /dev/null
+++ b/src/pages/WriteSteps/ProductSelectStep.jsx
@@ -0,0 +1,45 @@
+import * as S from '../../styles/WriteSteps.styles';
+import { useProducts } from '../../hooks/network/product';
+import Button from '../../components/Button';
+import ButtonArea from '../../components/ButtonArea';
+
+function ProductSelectStep({ gotoNextStep, product, setProduct }) {
+ const { data: products } = useProducts();
+
+ return (
+
+ 판매할 휴대폰을 골라주세요
+ 모델
+ setProduct(e.target.value)}
+ >
+
+ {products.map(({ id, name }) => (
+
+ ))}
+
+
+
+
+
+ );
+}
+
+export default ProductSelectStep;
diff --git a/src/pages/WriteSteps/StatusStep.jsx b/src/pages/WriteSteps/StatusStep.jsx
new file mode 100644
index 0000000..418a77a
--- /dev/null
+++ b/src/pages/WriteSteps/StatusStep.jsx
@@ -0,0 +1,70 @@
+import Button from '../../components/Button';
+import ButtonArea from '../../components/ButtonArea';
+import CheckBox from '../../components/CheckBox';
+import * as S from '../../styles/WriteSteps.styles';
+
+function StatusStep({ gotoNextStep, gotoPrevStep, status, setStatus }) {
+ return (
+
+ 휴대폰 상태에 대해 알려주세요
+
+
+ setStatus((prev) => ({ ...prev, display: value }))
+ }
+ />
+
+ setStatus((prev) => ({ ...prev, back: value }))
+ }
+ />
+
+ setStatus((prev) => ({ ...prev, button: value }))
+ }
+ />
+
+ setStatus((prev) => ({ ...prev, biometrics: value }))
+ }
+ />
+
+ setStatus((prev) => ({ ...prev, others: value }))
+ }
+ />
+
+
+
+
+
+
+ );
+}
+
+export default StatusStep;
diff --git a/src/pages/WriteSteps/TextStep.jsx b/src/pages/WriteSteps/TextStep.jsx
new file mode 100644
index 0000000..93d70b5
--- /dev/null
+++ b/src/pages/WriteSteps/TextStep.jsx
@@ -0,0 +1,40 @@
+import { useState } from 'react';
+import Button from '../../components/Button';
+import ButtonArea from '../../components/ButtonArea';
+import * as S from '../../styles/WriteSteps.styles';
+
+function TextStep({ text, setText, gotoNextStep, gotoPrevStep }) {
+ const [height, setHeight] = useState(0);
+ const handleChange = (event) => {
+ console.log(event);
+ setText(event.target.value);
+ setHeight(event.target.scrollHeight);
+ };
+ return (
+
+ 자세한 내용을 적어주세요
+
+
+
+
+
+
+ );
+}
+
+export default TextStep;
diff --git a/src/pages/WriteSteps/index.jsx b/src/pages/WriteSteps/index.jsx
new file mode 100644
index 0000000..52bdfd3
--- /dev/null
+++ b/src/pages/WriteSteps/index.jsx
@@ -0,0 +1,85 @@
+import { useState } from 'react';
+import ProductSelectStep from './ProductSelectStep';
+import StatusStep from './StatusStep';
+import PhotoStep from './PhotoStep';
+import PriceStep from './PriceStep';
+import TextStep from './TextStep';
+
+function WriteSteps() {
+ const [formData, setFormData] = useState({
+ product: '',
+ status: {
+ display: false,
+ back: false,
+ button: false,
+ biometrics: false,
+ others: false,
+ },
+ price: '',
+ photos: [],
+ text: '',
+ });
+ const STEP_INFO = ['product', 'status', 'price', 'photo', 'text', 'step6'];
+ const setProduct = (product) => {
+ setFormData((prev) => ({ ...prev, product }));
+ };
+ const setStatus = (status) => {
+ setFormData((prev) => ({ ...prev, status }));
+ };
+ const setPrice = (price) => {
+ setFormData((prev) => ({ ...prev, price }));
+ };
+ const setPhotos = (photos) => {
+ setFormData((prev) => ({ ...prev, photos }));
+ };
+ const setText = (text) => {
+ setFormData((prev) => ({ ...prev, text }));
+ };
+ const [step, setStep] = useState(STEP_INFO[0]);
+
+ return (
+ <>
+ {step === 'product' && (
+ setStep('status')}
+ product={formData.product}
+ setProduct={setProduct}
+ />
+ )}
+ {step === 'status' && (
+ setStep('price')}
+ gotoPrevStep={() => setStep('product')}
+ />
+ )}
+ {step === 'price' && (
+ setStep('photo')}
+ gotoPrevStep={() => setStep('status')}
+ />
+ )}
+ {step === 'photo' && (
+ setStep('text')}
+ gotoPrevStep={() => setStep('price')}
+ />
+ )}
+ {step === 'text' && (
+ setStep('step6')}
+ gotoPrevStep={() => setStep('photo')}
+ />
+ )}
+ >
+ );
+}
+
+export default WriteSteps;
diff --git a/src/styles/Brand.styles.js b/src/styles/Brand.styles.js
new file mode 100644
index 0000000..eeb83ec
--- /dev/null
+++ b/src/styles/Brand.styles.js
@@ -0,0 +1,27 @@
+import { Link } from 'react-router-dom';
+import styled from 'styled-components';
+
+export const BrandList = styled.div`
+ ${({ theme }) => theme.maxWidth};
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: center;
+ padding: 0 20px;
+`;
+
+export const BrandItem = styled(Link)`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 150px;
+ height: 150px;
+ margin: 20px;
+ text-decoration: none;
+ color: #000;
+ background-color: #eee;
+ border-radius: 20px;
+ font-size: 20px;
+ font-weight: bold;
+ justify-content: center;
+`;
diff --git a/src/styles/Button.styles.js b/src/styles/Button.styles.js
new file mode 100644
index 0000000..80fba75
--- /dev/null
+++ b/src/styles/Button.styles.js
@@ -0,0 +1,32 @@
+import styled from 'styled-components';
+
+const BaseButton = styled.button`
+ width: 100%;
+ padding: 20px;
+ border-radius: 10px;
+ border: none;
+ font-size: 16px;
+ font-weight: 700;
+ color: ${({ theme }) => theme.colors.gray50};
+ transition: all 0.3s ease;
+ cursor: pointer;
+
+ &:active {
+ transform: scale(0.99);
+ }
+
+ &:disabled {
+ background-color: ${({ theme }) => theme.colors.gray200};
+ color: ${({ theme }) => theme.colors.gray500};
+ cursor: not-allowed;
+ }
+`;
+
+export const Button = {
+ primary: styled(BaseButton)`
+ background-color: ${({ theme }) => theme.colors.primary};
+ `,
+ secondary: styled(BaseButton)`
+ background-color: ${({ theme }) => theme.colors.gray400};
+ `,
+};
diff --git a/src/styles/ButtonArea.styles.js b/src/styles/ButtonArea.styles.js
new file mode 100644
index 0000000..e9ec554
--- /dev/null
+++ b/src/styles/ButtonArea.styles.js
@@ -0,0 +1,19 @@
+import styled from 'styled-components';
+
+export const ButtonArea = styled.div`
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ padding: 20px;
+ border-top: 1px solid ${({ theme }) => theme.colors.gray100};
+ background-color: ${({ theme }) => theme.colors.background};
+ box-sizing: border-box;
+`;
+
+export const ButtonWrapper = styled.div`
+ ${({ theme }) => theme.maxWidth};
+ display: flex;
+ flex-direction: row;
+ gap: 20px;
+`;
diff --git a/src/styles/CheckBox.styles.js b/src/styles/CheckBox.styles.js
new file mode 100644
index 0000000..7dcd4c6
--- /dev/null
+++ b/src/styles/CheckBox.styles.js
@@ -0,0 +1,46 @@
+import styled from 'styled-components';
+
+export const CheckBox = styled.label`
+ ${({ theme }) => theme.boxShadow};
+ ${({ theme }) => theme.hoverBorder};
+ ${({ theme }) => theme.activeTransform};
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 16px;
+ user-select: none;
+ cursor: pointer;
+`;
+
+export const CheckInput = styled.input.attrs({ type: 'checkbox' })`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 20px;
+ height: 20px;
+ border: 1px solid ${({ theme }) => theme.colors.gray300};
+ border-radius: 4px;
+ transition: all 0.1s ease;
+
+ &:checked {
+ background-color: ${({ theme }) => theme.colors.primary};
+ }
+`;
+
+export const Info = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+`;
+
+export const Name = styled.div`
+ font-size: 16px;
+ font-weight: 600;
+ color: ${({ theme }) => theme.colors.gray900};
+`;
+
+export const Text = styled.div`
+ font-size: 14px;
+ font-weight: 400;
+ color: ${({ theme }) => theme.colors.gray500};
+`;
diff --git a/src/styles/Common.styles.js b/src/styles/Common.styles.js
new file mode 100644
index 0000000..3fa0558
--- /dev/null
+++ b/src/styles/Common.styles.js
@@ -0,0 +1,19 @@
+import { styled } from 'styled-components';
+
+export const Title = styled.h1`
+ font-size: 24px;
+ font-weight: 700;
+ padding: 32px 16px;
+ margin: 0;
+ user-select: none;
+ box-sizing: border-box;
+`;
+
+export const SubTitle = styled.h2`
+ font-size: 18px;
+ font-weight: 700;
+ padding: 16px;
+ margin: 0;
+ user-select: none;
+ box-sizing: border-box;
+`;
diff --git a/src/styles/CurrencyInput.styles.js b/src/styles/CurrencyInput.styles.js
new file mode 100644
index 0000000..5a507d9
--- /dev/null
+++ b/src/styles/CurrencyInput.styles.js
@@ -0,0 +1,30 @@
+import styled from 'styled-components';
+
+export const CurrencyInput = styled.div`
+ ${({ theme }) => theme.boxShadow};
+ ${({ theme }) => theme.hoverBorder};
+ ${({ theme }) => theme.activeTransform};
+ padding: 0;
+`;
+
+export const CurrencyInputField = styled.input.attrs({ type: 'text' })`
+ width: 100%;
+ height: 100%;
+ padding: 20px;
+ border-radius: 10px;
+ box-sizing: border-box;
+ color: ${({ theme }) => theme.colors.gray900};
+ font-size: 16px;
+ font-weight: 600;
+ caret-color: transparent;
+ border: none;
+
+ &:focus,
+ &:active {
+ outline: none;
+ }
+
+ &:placeholder-shown {
+ caret-color: ${({ theme }) => theme.colors.primary};
+ }
+`;
diff --git a/src/styles/Form.styles.js b/src/styles/Form.styles.js
new file mode 100644
index 0000000..781e397
--- /dev/null
+++ b/src/styles/Form.styles.js
@@ -0,0 +1,79 @@
+import { styled } from 'styled-components';
+
+export const Form = styled.form`
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: start;
+ gap: 16px;
+ width: 100%;
+ .formInput {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ min-height: 40px;
+ .helpText {
+ width: 20%;
+ min-width: 80px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ font-size: 16px;
+ }
+ .formInput textarea {
+ text-align: left;
+ }
+ .input {
+ width: 100%;
+ min-height: 40px;
+ margin: 0;
+ padding: 8px 16px;
+ box-sizing: border-box;
+ border-radius: 10px;
+ border: none;
+ background-color: ${({ theme }) => theme.colors.gray100};
+ }
+ .input:focus,
+ .input:focus-visible,
+ .input:focus-within {
+ outline: 0;
+ }
+ .input:disabled {
+ }
+ }
+ input[type='button'],
+ input[type='submit'] {
+ font-size: 16px;
+ font-weight: 600;
+ padding: 16px;
+ width: 100%;
+ min-height: 40px;
+ border-radius: 10px;
+ background-color: ${({ theme }) => theme.colors.gray400};
+ color: white;
+ border: none;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ &:disabled {
+ background-color: ${({ theme }) => theme.colors.gray300};
+ cursor: not-allowed;
+ }
+
+ &:active {
+ transform: scale(0.99);
+ }
+ }
+ input[type='submit'] {
+ background-color: ${({ theme }) => theme.colors.primary};
+ }
+`;
+
+export const Buttons = styled.div`
+ margin-top: 16px;
+ display: flex;
+ flex-direction: column;
+ justify-content: stretch;
+ gap: 16px;
+ width: 100%;
+`;
diff --git a/src/styles/Global.js b/src/styles/Global.js
new file mode 100644
index 0000000..9d9d1d9
--- /dev/null
+++ b/src/styles/Global.js
@@ -0,0 +1,23 @@
+import { createGlobalStyle } from 'styled-components';
+
+const GlobalStyle = createGlobalStyle`
+html,
+body {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ font-family: 'Noto Sans', Arial, sans-serif;
+ letter-spacing: -0.6px;
+ color: ${({ theme }) => theme.colors.gray900};
+}
+input, textarea, button, select {
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+}
+img {
+ user-select: none;
+}
+`;
+
+export default GlobalStyle;
diff --git a/src/styles/ImageViewer.style.js b/src/styles/ImageViewer.style.js
new file mode 100644
index 0000000..18a4958
--- /dev/null
+++ b/src/styles/ImageViewer.style.js
@@ -0,0 +1,29 @@
+import styled from 'styled-components';
+
+export const ImageViewer = styled.div`
+ ${({ theme }) => theme.boxShadow};
+`;
+
+export const MainImage = styled.img`
+ max-width: 100%;
+ width: 100%;
+ height: 400px;
+ object-fit: cover;
+ border-radius: 8px;
+`;
+
+export const Images = styled.div`
+ display: flex;
+ gap: 16px;
+ overflow-x: scroll;
+ margin-top: 20px;
+ /* justify-content: center; */
+
+ img {
+ width: 64px;
+ height: 64px;
+ object-fit: cover;
+ border-radius: 8px;
+ cursor: pointer;
+ }
+`;
diff --git a/src/styles/KeyFrames.styles.js b/src/styles/KeyFrames.styles.js
new file mode 100644
index 0000000..061825f
--- /dev/null
+++ b/src/styles/KeyFrames.styles.js
@@ -0,0 +1,12 @@
+import { keyframes } from 'styled-components';
+
+export const FadeInSlideFromRightAnimation = keyframes`
+ from {
+ opacity: 0;
+ transform: translateX(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+`
diff --git a/src/styles/Layout.styles.js b/src/styles/Layout.styles.js
new file mode 100644
index 0000000..c7f02f7
--- /dev/null
+++ b/src/styles/Layout.styles.js
@@ -0,0 +1,92 @@
+// import LogoImage from '../assets/logo.svg';
+import { Link } from 'react-router-dom';
+import { styled } from 'styled-components';
+
+export const Header = styled.header`
+ position: sticky;
+ top: 0;
+ width: 100%;
+ height: 60px;
+ background-color: ${({ theme }) => theme.colors.background};
+ border-bottom: ${({ theme }) => `1px solid ${theme.colors.gray200}`};
+ text-align: center;
+ user-select: none;
+ z-index: 9999;
+
+ .content {
+ ${({ theme }) => theme.maxWidth};
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ gap: 32px;
+ align-items: center;
+ padding: 0 32px;
+ height: 100%;
+ }
+`;
+
+export const Logo = styled(Link)`
+ max-width: 100px;
+ height: 40px;
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+`;
+
+export const Nav = styled.nav`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ height: 100%;
+ background-color: #fff;
+ position: sticky;
+ top: 0;
+ z-index: 9998;
+
+ > .left,
+ > .right {
+ display: flex;
+ gap: 8px;
+ }
+`;
+
+export const NavItem = styled(Link)`
+ color: ${({ theme }) => theme.colors.gray700};
+ font-size: 16px;
+ text-decoration: none;
+ border-radius: 8px;
+ padding: 8px;
+ box-sizing: border-box;
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background-color: ${({ theme }) => theme.colors.gray200};
+ }
+`;
+
+export const Main = styled.main`
+ ${({ theme }) => theme.maxWidth};
+ min-height: calc(100vh - 120px);
+ padding: 32px;
+`;
+
+export const Footer = styled.footer`
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ justify-content: center;
+ box-sizing: border-box;
+ width: 100%;
+ height: 60px;
+ color: ${({ theme }) => theme.colors.gray500};
+ font-size: 12px;
+`;
+
+export const FooterWrapper = styled.div`
+ ${({ theme }) => theme.maxWidth};
+ padding: 0 32px;
+`;
diff --git a/src/styles/MyPage.styles.js b/src/styles/MyPage.styles.js
new file mode 100644
index 0000000..0e6ce3c
--- /dev/null
+++ b/src/styles/MyPage.styles.js
@@ -0,0 +1,32 @@
+import { Link } from 'react-router-dom';
+import styled from 'styled-components';
+
+export const MyPage = styled.div`
+ ${({ theme }) => theme.maxWidth};
+ display: flex;
+ flex-direction: column;
+`;
+
+export const Section = styled.section`
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 0 16px 16px;
+`;
+
+export const SectionContents = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+`;
+
+export const LinkItem = styled(Link)`
+ ${({ theme }) => theme.boxShadow};
+ ${({ theme }) => theme.hoverBorder};
+ ${({ theme }) => theme.activeTransform};
+ display: block;
+ font-size: 16px;
+ padding: 16px 20px;
+ color: ${({ theme }) => theme.colors.gray900};
+ text-decoration: none;
+`;
diff --git a/src/styles/PanelLayout.styles.js b/src/styles/PanelLayout.styles.js
new file mode 100644
index 0000000..1bb0334
--- /dev/null
+++ b/src/styles/PanelLayout.styles.js
@@ -0,0 +1,34 @@
+import styled from 'styled-components';
+
+export const PanelLayout = styled.div`
+ display: flex;
+ flex-direction: row;
+ gap: 32px;
+
+ @media screen and (max-width: 768px) {
+ flex-direction: ${({ $reverse }) =>
+ $reverse ? 'column-reverse' : 'column'};
+ gap: 10px;
+ }
+`;
+
+export const LeftPanel = styled.div`
+ width: 60%;
+
+ @media screen and (max-width: 768px) {
+ width: 100%;
+ }
+`;
+
+export const RightPanel = styled.div`
+ width: 40%;
+ /* height: 100%; */
+ height: fit-content;
+ position: sticky;
+ top: 100px;
+
+ @media screen and (max-width: 768px) {
+ position: static;
+ width: 100%;
+ }
+`;
diff --git a/src/styles/PostDetail.styles.js b/src/styles/PostDetail.styles.js
new file mode 100644
index 0000000..eb1a027
--- /dev/null
+++ b/src/styles/PostDetail.styles.js
@@ -0,0 +1,42 @@
+import { styled } from 'styled-components';
+
+export const PostDetail = styled.div`
+ ${({ theme }) => theme.maxWidth};
+
+ .product {
+ display: inline-block;
+ color: ${({ theme }) => theme.colors.gray900};
+ text-decoration: none;
+ font-size: 24px;
+ font-weight: 600;
+ }
+
+ time {
+ display: block;
+ margin-top: 4px;
+ font-size: 14px;
+ line-height: 1.5;
+ letter-spacing: 0;
+ color: ${({ theme }) => theme.colors.gray500};
+ }
+
+ .price {
+ margin-top: 8px;
+ font-size: 18px;
+ line-height: 1.7;
+ font-weight: bold;
+ }
+
+ p {
+ font-size: 16px;
+ }
+`;
+
+export const WrittenTime = styled.time`
+ display: block;
+ margin-top: 4px;
+ font-size: 14px;
+ line-height: 1.5;
+ letter-spacing: 0;
+ color: ${({ theme }) => theme.colors.gray500};
+`;
diff --git a/src/styles/PostItem.styles.js b/src/styles/PostItem.styles.js
new file mode 100644
index 0000000..47f1bf3
--- /dev/null
+++ b/src/styles/PostItem.styles.js
@@ -0,0 +1,50 @@
+import { Link } from 'react-router-dom';
+import { styled } from 'styled-components';
+
+export const PostItem = styled(Link)`
+ ${({ theme }) => theme.boxShadow};
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ padding: 16px;
+ margin-bottom: 16px;
+ box-sizing: border-box;
+ width: 100%;
+ text-decoration: none;
+ color: ${({ theme }) => theme.colors.gray900};
+ transition: all 0.3s ease;
+`;
+
+export const ProductInfo = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+export const ProductName = styled.div`
+ font-size: 19.2px;
+ font-weight: 600;
+`;
+
+export const ProductDate = styled.time`
+ font-size: 12.8px;
+ color: ${({ theme }) => theme.colors.gray400};
+ margin-bottom: 12px;
+`;
+
+export const ProductPrice = styled.div`
+ font-size: 16px;
+ font-weight: 600;
+`;
+
+export const PostImage = styled.div`
+ width: 100px;
+ height: 100px;
+ border-radius: 10px;
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 10px;
+ }
+`;
diff --git a/src/styles/Posts.styles.js b/src/styles/Posts.styles.js
new file mode 100644
index 0000000..d3aea79
--- /dev/null
+++ b/src/styles/Posts.styles.js
@@ -0,0 +1,5 @@
+import { styled } from 'styled-components';
+
+export const Posts = styled.div`
+ margin: 0 auto;
+`;
diff --git a/src/styles/ProductPriceGraph.styles.js b/src/styles/ProductPriceGraph.styles.js
new file mode 100644
index 0000000..6b88271
--- /dev/null
+++ b/src/styles/ProductPriceGraph.styles.js
@@ -0,0 +1,26 @@
+import styled from 'styled-components';
+
+export const ProductPriceGraph = styled.div`
+ ${({ theme }) => theme.boxShadow};
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ width: 100%;
+ box-sizing: border-box;
+
+ &:hover,
+ &:active {
+ border-color: ${({ theme }) => theme.colors.gray200};
+ transform: none;
+ }
+`;
+
+export const GraphArea = styled.div`
+ width: 100%;
+ height: 200px;
+`;
+
+export const Text = styled.div`
+ font-size: 16px;
+ margin-left: 16px;
+`;
diff --git a/src/styles/Products.styles.js b/src/styles/Products.styles.js
new file mode 100644
index 0000000..666341d
--- /dev/null
+++ b/src/styles/Products.styles.js
@@ -0,0 +1,16 @@
+import { Link } from 'react-router-dom';
+import { styled } from 'styled-components';
+
+export const ProductItem = styled(Link)`
+ ${({ theme }) => theme.boxShadow};
+ display: block;
+ padding: 16px;
+ margin-bottom: 16px;
+ height: 100%;
+ text-decoration: none;
+ color: ${({ theme }) => theme.colors.gray900};
+ transition: all 0.2s ease;
+ user-select: none;
+ font-size: 16px;
+ font-weight: 500;
+`;
diff --git a/src/styles/Themes.styles.js b/src/styles/Themes.styles.js
new file mode 100644
index 0000000..3d23e04
--- /dev/null
+++ b/src/styles/Themes.styles.js
@@ -0,0 +1,55 @@
+import { css } from 'styled-components';
+
+const colors = {
+ gray50: '#f9fafb',
+ gray100: '#f2f4f6',
+ gray200: '#e5e8eb',
+ gray300: '#d1d6db',
+ gray400: '#b0b8c1',
+ gray500: '#8b95a1',
+ gray600: '#6b7684',
+ gray700: '#4e5968',
+ gray800: '#333d4b',
+ gray900: '#191f28',
+ background: '#fff',
+ grayBackground: '#f2f4f6',
+ primary: '#2060ee',
+};
+
+const boxShadow = css`
+ padding: 20px;
+ border-radius: 10px;
+ border: 1px solid ${({ theme }) => theme.colors.gray200};
+ box-shadow: 0px 4px 20px 0px #2060ee1a;
+ transition: all 0.3s ease;
+`;
+
+const hoverBorder = css`
+ &:hover,
+ &:active {
+ border-color: ${({ theme }) => theme.colors.primary};
+ }
+`;
+
+const activeTransform = css`
+ &:active {
+ transform: scale(0.99);
+ }
+`;
+
+const maxWidth = css`
+ width: 100%;
+ max-width: 1200px;
+ margin: 0 auto;
+ box-sizing: border-box;
+`;
+
+const theme = {
+ colors,
+ boxShadow,
+ hoverBorder,
+ activeTransform,
+ maxWidth,
+};
+
+export default theme;
diff --git a/src/styles/WriteSteps.styles.js b/src/styles/WriteSteps.styles.js
new file mode 100644
index 0000000..9e489e0
--- /dev/null
+++ b/src/styles/WriteSteps.styles.js
@@ -0,0 +1,65 @@
+import styled from 'styled-components';
+
+export const Step = styled.div`
+ /* flex-grow: 1; */
+ min-height: calc(100vh - 240px);
+ display: flex;
+ flex-direction: column;
+ justify-content: stretch;
+ width: 100%;
+`;
+
+export const Title = styled.h1`
+ margin: 0;
+ font-size: 24px;
+ font-weight: 700;
+ line-height: 1.44;
+ color: ${({ theme }) => theme.colors.gray900};
+ word-break: keep-all;
+ margin-bottom: 40px;
+`;
+
+export const HelpText = styled.div`
+ display: block;
+ margin-bottom: 16px;
+ margin-left: 8px;
+ font-size: 16px;
+ font-weight: 600;
+ color: ${({ theme }) => theme.colors.gray900};
+`;
+
+export const Select = styled.select`
+ ${({ theme }) => theme.boxShadow};
+ width: 100%;
+ box-sizing: border-box;
+ color: ${({ theme }) => theme.colors.gray900};
+ cursor: pointer;
+`;
+
+export const Grid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20px;
+
+ @media screen and (max-width: 768px) {
+ grid-template-columns: repeat(1, 1fr);
+ }
+`;
+
+export const TextArea = styled.textarea`
+ ${({ theme }) => theme.boxShadow};
+ flex: 1;
+ width: 100%;
+ min-height: 160px;
+ padding: 16px;
+ font-size: 16px;
+ font-weight: 400;
+ color: ${({ theme }) => theme.colors.gray900};
+ box-sizing: border-box;
+ transition: all 0.3s ease;
+ outline: none;
+
+ &:active {
+ transform: none;
+ }
+`;