diff --git a/vite-project/.env.development b/vite-project/.env.development new file mode 100644 index 00000000..887a088a --- /dev/null +++ b/vite-project/.env.development @@ -0,0 +1 @@ +VITE_API_BASE_URL=https://panda-market-api.vercel.app \ No newline at end of file diff --git a/vite-project/.env.production b/vite-project/.env.production new file mode 100644 index 00000000..887a088a --- /dev/null +++ b/vite-project/.env.production @@ -0,0 +1 @@ +VITE_API_BASE_URL=https://panda-market-api.vercel.app \ No newline at end of file diff --git a/vite-project/package-lock.json b/vite-project/package-lock.json index e8e37d18..2d849630 100644 --- a/vite-project/package-lock.json +++ b/vite-project/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router-dom": "^7.7.0" + "react-router-dom": "^7.7.0", + "styled-components": "^6.1.19" }, "devDependencies": { "@eslint/js": "^9.25.0", @@ -320,6 +321,27 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", @@ -1387,6 +1409,12 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1532,6 +1560,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001727", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", @@ -1628,11 +1665,30 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -2273,7 +2329,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -2389,7 +2444,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2434,6 +2488,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2595,6 +2655,12 @@ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "license": "MIT" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2622,7 +2688,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -2641,6 +2706,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.1.19", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz", + "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2671,6 +2798,12 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/vite-project/package.json b/vite-project/package.json index ca510320..85b90b29 100644 --- a/vite-project/package.json +++ b/vite-project/package.json @@ -12,7 +12,8 @@ "dependencies": { "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router-dom": "^7.7.0" + "react-router-dom": "^7.7.0", + "styled-components": "^6.1.19" }, "devDependencies": { "@eslint/js": "^9.25.0", diff --git a/vite-project/src/api/products.js b/vite-project/src/api/products.js index 1a1cd7d5..fe32ac72 100644 --- a/vite-project/src/api/products.js +++ b/vite-project/src/api/products.js @@ -1,12 +1,10 @@ -const BASE_URL = "https://panda-market-api.vercel.app"; - export async function fetchProducts({ keyword = "", page = 1, pageSize = 10, orderBy = "recent", } = {}) { - const url = new URL(`${BASE_URL}/products`); + const url = new URL(`${import.meta.env.VITE_API_BASE_URL}/products`); url.searchParams.append("page", page); url.searchParams.append("pageSize", pageSize); url.searchParams.append("orderBy", orderBy); diff --git a/vite-project/src/assets/ic-plus-gray.svg b/vite-project/src/assets/ic-plus-gray.svg new file mode 100644 index 00000000..5bb9abf5 --- /dev/null +++ b/vite-project/src/assets/ic-plus-gray.svg @@ -0,0 +1,4 @@ + + + + diff --git a/vite-project/src/assets/ic-xmark-fill.svg b/vite-project/src/assets/ic-xmark-fill.svg new file mode 100644 index 00000000..87a00547 --- /dev/null +++ b/vite-project/src/assets/ic-xmark-fill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/vite-project/src/components/Button.css b/vite-project/src/components/Button.css deleted file mode 100644 index 35f0f0c2..00000000 --- a/vite-project/src/components/Button.css +++ /dev/null @@ -1,13 +0,0 @@ -@import url("../palette.css"); - -.Button { - background-color: var(--color-primary-100); - padding: 8px 23px; - color: var(--color-cool-gray-100); - font-size: 16px; - font-weight: 600; - line-height: 26px; - border-radius: 8px; - border: none; - cursor: pointer; -} diff --git a/vite-project/src/components/Button.jsx b/vite-project/src/components/Button.jsx index 9f9facb4..4019d336 100644 --- a/vite-project/src/components/Button.jsx +++ b/vite-project/src/components/Button.jsx @@ -1,11 +1,20 @@ -import "./Button.css"; +import styled from "styled-components"; -function Button({ title, onClick }) { - return ( - - ); +const StyledButton = styled.button` + background-color: ${({ disabled }) => + disabled ? "var(--color-cool-gray-400)" : "var(--color-primary-100)"}; + padding: 8px 23px; + color: var(--color-cool-gray-100); + font-size: 16px; + font-weight: 600; + line-height: 26px; + border-radius: 8px; + border: none; + cursor: ${({ disabled }) => (disabled ? "default" : "pointer")}; +`; + +function Button({ children, ...props }) { + return {children}; } export default Button; diff --git a/vite-project/src/components/Item.css b/vite-project/src/components/Item.css deleted file mode 100644 index 809d05c2..00000000 --- a/vite-project/src/components/Item.css +++ /dev/null @@ -1,51 +0,0 @@ -@import url("../palette.css"); - -.Item { - color: var(--color-secondary-800); - display: flex; - flex-direction: column; - gap: 16px; -} - -.Item-image { - border-radius: 16px; - aspect-ratio: 1 / 1; - overflow: hidden; -} - -.Item-image img { - width: 100%; - height: 100%; - aspect-ratio: 1 / 1; -} - -.Item-info { - display: flex; - flex-direction: column; - gap: 6px; -} - -.Item-info p { - margin: 0; -} - -.Item-title { - font-size: 14px; - font-weight: 500; - line-height: 24px; -} - -.Item-price { - font-size: 16px; - font-weight: 700; - line-height: 26px; -} - -.Item-likes { - font-size: 12px; - font-weight: 500; - line-height: 18px; - display: flex; - gap: 4px; - align-items: center; -} diff --git a/vite-project/src/components/Item.jsx b/vite-project/src/components/Item.jsx deleted file mode 100644 index 679b28ce..00000000 --- a/vite-project/src/components/Item.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import likeImg from "../assets/ic-heart.svg"; -import "./Item.css"; - -function Item({ imageUrl, title, price, likeCount }) { - const priceString = Intl.NumberFormat().format(price) + "원"; - return ( -
-
- 상품 이미지 -
-
-

{title}

-

{priceString}

-
- 좋아요 갯수 - {likeCount} -
-
-
- ); -} - -export default Item; diff --git a/vite-project/src/components/ItemsSection.css b/vite-project/src/components/ItemsSection.css deleted file mode 100644 index a2bc17f3..00000000 --- a/vite-project/src/components/ItemsSection.css +++ /dev/null @@ -1,72 +0,0 @@ -@import url("../palette.css"); - -.ItemsSection { - width: 100%; - display: flex; - flex-direction: column; -} - -/* ItemsSectionHeader */ - -.ItemsSectionHeader { - display: flex; - justify-content: space-between; - align-items: center; -} - -.ItemsSectionHeader-title { - font-size: 20px; - font-weight: 700; - line-height: 32px; - color: var(--color-secondary-900); - margin: 0; -} - -.ItemsSectionHeader-controls { - display: flex; - gap: 12px; -} - -/* ItemsSectionContent */ - -.ItemsSectionContent { - display: grid; - column-gap: 24px; - row-gap: 40px; -} - -/* Tablet */ -@media (max-width: 1199px) { - .SearchInput { - width: 242px; - } -} - -/* Mobile */ -@media (max-width: 767px) { - .SearchInput { - width: 100%; - flex-grow: 1; - } - - .ItemsSectionHeader { - flex-direction: column; - align-items: flex-start; - gap: 8px; - position: relative; - } - - .ItemsSectionHeader-title { - line-height: 42px; - } - - .ItemsSectionHeader-controls { - width: 100%; - gap: 14px; - } - .ItemsSectionHeader-controls button:nth-child(2) { - position: absolute; - top: 0; - right: 0; - } -} diff --git a/vite-project/src/components/ItemsSection.jsx b/vite-project/src/components/ItemsSection.jsx deleted file mode 100644 index f0844277..00000000 --- a/vite-project/src/components/ItemsSection.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import Item from "./Item"; -import "./ItemsSection.css"; - -function ItemsSectionHeader({ title, children }) { - return ( -
-

{title}

- {children && ( -
{children}
- )} -
- ); -} - -function ItemsSectionContent({ items, numberOfColumns }) { - return ( -
- {items.map((product) => ( - - ))} -
- ); -} - -function ItemsSection({ spacing = 16, children }) { - return ( -
- {children} -
- ); -} - -export { ItemsSection, ItemsSectionContent, ItemsSectionHeader }; diff --git a/vite-project/src/components/Nav.jsx b/vite-project/src/components/Nav.jsx index 855087d7..ed76fa9c 100644 --- a/vite-project/src/components/Nav.jsx +++ b/vite-project/src/components/Nav.jsx @@ -1,16 +1,30 @@ -import { NavLink } from "react-router-dom"; +import { NavLink, useLocation } from "react-router-dom"; import largeLogo from "../assets/logo-large.svg"; import smallLogo from "../assets/logo-small.svg"; import profileImg from "../assets/profile-default.svg"; import "./Nav.css"; -export default function Nav() { - const linkStyle = ({ isActive }) => ({ +function NavigationLink({ to, activePaths = [], children }) { + const location = useLocation(); + + const linkStyle = (isActive) => ({ color: `var(--color-${isActive ? "primary-100" : "secondary-600"})`, textDecoration: "none", }); + const active = activePaths.includes(location.pathname); + + return ( +
  • + linkStyle(isActive || active)}> + {children} + +
  • + ); +} + +function Nav() { return ( ); } + +export default Nav; diff --git a/vite-project/src/components/PageControl.jsx b/vite-project/src/components/PageControl.jsx index 51198d71..520af5d1 100644 --- a/vite-project/src/components/PageControl.jsx +++ b/vite-project/src/components/PageControl.jsx @@ -1,6 +1,6 @@ import chevronLeftImg from "../assets/ic-chevron-left.svg"; import chevronRightImg from "../assets/ic-chevron-right.svg"; -import PageController from "../utils/pageController"; +import PageController from "../utils/PageController"; import "./PageControl.css"; const PAGE_MOVE_IMAGE = { diff --git a/vite-project/src/components/SearchInput.css b/vite-project/src/components/SearchInput.css index fc9b9ae8..bf0b8a14 100644 --- a/vite-project/src/components/SearchInput.css +++ b/vite-project/src/components/SearchInput.css @@ -37,3 +37,11 @@ width: 242px; } } + +/* Mobile */ +@media (max-width: 767px) { + .SearchInput { + width: 100%; + flex-grow: 1; + } +} diff --git a/vite-project/src/components/add-item/adding-item-image-container.jsx b/vite-project/src/components/add-item/adding-item-image-container.jsx new file mode 100644 index 00000000..f656349c --- /dev/null +++ b/vite-project/src/components/add-item/adding-item-image-container.jsx @@ -0,0 +1,77 @@ +import { useEffect, useRef, useState } from "react"; +import styled from "styled-components"; +import AddingItemImage from "./adding-item-image"; +import AddingItemImageInput from "./adding-item-image-input"; + +const StyledAddingItemImageContainer = styled.div` + display: flex; + gap: 24px; + + @media (max-width: 1199px) { + gap: 10px; + } +`; + +const StyledErrorMessage = styled.p` + color: var(--color-error-red); + font-size: 16px; + font-weight: 400; + line-height: 26px; + margin: 16px 0 0; +`; + +function AddingItemImageContainer() { + const inputRef = useRef(); + const [preview, setPreview] = useState(); + const [error, setError] = useState(false); + + const handleInputClick = (event) => { + if (!preview) return; + setError(true); + event.preventDefault(); + }; + + const handleInputChange = (file) => { + if (!file) return; + const nextPreview = URL.createObjectURL(file); + setPreview(nextPreview); + }; + + const handleRemovePreview = () => { + URL.revokeObjectURL(preview); + setPreview(null); + setError(false); + inputRef.current.value = ""; + }; + + useEffect(() => { + return () => { + if (!preview) return; + setPreview(null); + URL.revokeObjectURL(preview); + }; + }, [preview]); + + return ( +
    + + + {preview && ( + + )} + + {error && ( + + *이미지 등록은 최대 1개까지 가능합니다. + + )} +
    + ); +} + +export default AddingItemImageContainer; diff --git a/vite-project/src/components/add-item/adding-item-image-input.jsx b/vite-project/src/components/add-item/adding-item-image-input.jsx new file mode 100644 index 00000000..ce03c4a5 --- /dev/null +++ b/vite-project/src/components/add-item/adding-item-image-input.jsx @@ -0,0 +1,51 @@ +import styled from "styled-components"; +import addImg from "../../assets/ic-plus-gray.svg"; + +const StyledAddingItemImageLabel = styled.label` + background-color: var(--color-secondary-100); + color: var(--color-cool-gray-400); + font-size: 16px; + font-weight: 400; + line-height: 26px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 12px; + border: none; + cursor: pointer; + + width: 282px; + aspect-ratio: 1; + border-radius: 12px; + + @media (max-width: 1199px) { + width: 168px; + } + + @media (max-width: 767px) { + width: 50%; + } +`; + +function AddingItemImageInput({ onClick, onChange, ref }) { + return ( + + 상품 이미지 추가 아이콘 + 이미지 등록 + { + onChange(target.files[0]); + }} + ref={ref} + /> + + ); +} + +export default AddingItemImageInput; diff --git a/vite-project/src/components/add-item/adding-item-image.jsx b/vite-project/src/components/add-item/adding-item-image.jsx new file mode 100644 index 00000000..8c4f3b8c --- /dev/null +++ b/vite-project/src/components/add-item/adding-item-image.jsx @@ -0,0 +1,46 @@ +import styled from "styled-components"; +import removeImg from "../../assets/ic-xmark-fill.svg"; + +const StyledAddingItemImage = styled.div` + width: 282px; + position: relative; + + & > img { + width: 100%; + aspect-ratio: 1; + object-fit: cover; + border-radius: 12px; + } + + & > button { + margin: 0; + padding: 0; + border: none; + background: none; + cursor: pointer; + position: absolute; + top: 12px; + right: 12px; + } + + @media (max-width: 1199px) { + width: 168px; + } + + @media (max-width: 767px) { + width: 50%; + } +`; + +function AddingItemImage({ imageUrl, onRemove }) { + return ( + + {imageUrl && 등록할 상품 이미지} + + + ); +} + +export default AddingItemImage; diff --git a/vite-project/src/components/item/item.jsx b/vite-project/src/components/item/item.jsx new file mode 100644 index 00000000..cb0fed39 --- /dev/null +++ b/vite-project/src/components/item/item.jsx @@ -0,0 +1,71 @@ +import styled from "styled-components"; +import likeImg from "../../assets/ic-heart.svg"; +import { formatPrice } from "../../utils/formatter"; + +const StyledItem = styled.div` + color: var(--color-secondary-800); + display: flex; + flex-direction: column; + gap: 16px; +`; + +const StyledItemImage = styled.div` + border-radius: 16px; + aspect-ratio: 1 / 1; + overflow: hidden; + + img { + width: 100%; + height: 100%; + aspect-ratio: 1 / 1; + } +`; + +const StyledItemFavorites = styled.div` + font-size: 12px; + font-weight: 500; + line-height: 18px; + display: flex; + gap: 4px; + align-items: center; +`; + +const StyledItemInfo = styled.div` + display: flex; + flex-direction: column; + gap: 6px; + + h3 { + font-size: 14px; + font-weight: 500; + line-height: 24px; + margin: 0; + } + + p { + font-size: 16px; + font-weight: 700; + line-height: 26px; + margin: 0; + } +`; + +function Item({ imageUrl, title, price, likeCount }) { + return ( + + + 상품 이미지 + + +

    {title}

    +

    {formatPrice(price, "원")}

    + + 좋아요 갯수 + {likeCount} + +
    +
    + ); +} + +export default Item; diff --git a/vite-project/src/components/item/items-grid.jsx b/vite-project/src/components/item/items-grid.jsx new file mode 100644 index 00000000..ceab0b45 --- /dev/null +++ b/vite-project/src/components/item/items-grid.jsx @@ -0,0 +1,30 @@ +import styled from "styled-components"; +import Item from "./item"; + +const StyledItemsGrid = styled.div` + display: grid; + grid-template-columns: repeat( + ${({ $numberOfColumns }) => $numberOfColumns}, + 1fr + ); + column-gap: 24px; + row-gap: 40px; +`; + +function ItemsGrid({ items, numberOfColumns }) { + return ( + + {items.map((product) => ( + + ))} + + ); +} + +export default ItemsGrid; diff --git a/vite-project/src/components/section/section-header-action.jsx b/vite-project/src/components/section/section-header-action.jsx new file mode 100644 index 00000000..26422767 --- /dev/null +++ b/vite-project/src/components/section/section-header-action.jsx @@ -0,0 +1,18 @@ +import styled from "styled-components"; +import Button from "../Button"; + +const StyledSectionHeaderAction = styled(Button)` + @media (max-width: 767px) { + position: absolute; + top: 0; + right: 0; + } +`; + +function SectionHeaderAction({ children, ...props }) { + return ( + {children} + ); +} + +export default SectionHeaderAction; diff --git a/vite-project/src/components/section/section-header-size.js b/vite-project/src/components/section/section-header-size.js new file mode 100644 index 00000000..01e35b9e --- /dev/null +++ b/vite-project/src/components/section/section-header-size.js @@ -0,0 +1,8 @@ +const SectionHeaderSize = { + LARGE: "large", + SMALL: "small", +}; + +Object.freeze(SectionHeaderSize); + +export default SectionHeaderSize; diff --git a/vite-project/src/components/section/section-header-title.jsx b/vite-project/src/components/section/section-header-title.jsx new file mode 100644 index 00000000..27eac49e --- /dev/null +++ b/vite-project/src/components/section/section-header-title.jsx @@ -0,0 +1,45 @@ +import styled, { css } from "styled-components"; +import SectionHeaderSize from "./section-header-size"; + +const headerTitleStyle = css` + font-weight: 700; + color: var(--color-secondary-900); + margin: 0; + + @media (max-width: 767px) { + line-height: 42px; + } +`; + +const StyledSectionHeaderTitleLarge = styled.h2` + ${headerTitleStyle} + font-size: 20px; + line-height: 32px; +`; + +const StyledSectionHeaderTitleSmall = styled.h3` + ${headerTitleStyle} + font-size: 18px; + line-height: 26px; +`; + +function SectionHeaderTitle({ size, children }) { + switch (size) { + case SectionHeaderSize.LARGE: + return ( + + {children} + + ); + case SectionHeaderSize.SMALL: + return ( + + {children} + + ); + default: + return; + } +} + +export default SectionHeaderTitle; diff --git a/vite-project/src/components/section/section-header.jsx b/vite-project/src/components/section/section-header.jsx new file mode 100644 index 00000000..0a419dde --- /dev/null +++ b/vite-project/src/components/section/section-header.jsx @@ -0,0 +1,41 @@ +import styled from "styled-components"; +import SectionHeaderSize from "./section-header-size"; +import SectionHeaderTitle from "./section-header-title"; + +const StyledSectionHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + @media (max-width: 767px) { + flex-direction: column; + align-items: flex-start; + ${({ $hasGap }) => ($hasGap ? "gap: 8px;" : "")} + position: relative; + } +`; + +const StyledSectionHeaderActions = styled.div` + display: flex; + gap: 12px; + + @media (max-width: 767px) { + width: 100%; + gap: 14px; + } +`; + +function SectionHeader({ children, title, size = SectionHeaderSize.LARGE }) { + const hasSingleAction = children?.type?.name === "SectionHeaderAction"; + + return ( + + {title && {title}} + {children && ( + {children} + )} + + ); +} + +export default SectionHeader; diff --git a/vite-project/src/components/section/section.jsx b/vite-project/src/components/section/section.jsx new file mode 100644 index 00000000..0d479ad4 --- /dev/null +++ b/vite-project/src/components/section/section.jsx @@ -0,0 +1,18 @@ +import styled from "styled-components"; + +const StyledSection = styled.section` + width: 100%; + display: flex; + flex-direction: column; + gap: ${({ $spacing }) => $spacing}px; + + @media (max-width: 767px) { + gap: 16px; + } +`; + +function Section({ children, spacing = 16 }) { + return {children}; +} + +export default Section; diff --git a/vite-project/src/components/tag/tag-list.jsx b/vite-project/src/components/tag/tag-list.jsx new file mode 100644 index 00000000..a6e64e3e --- /dev/null +++ b/vite-project/src/components/tag/tag-list.jsx @@ -0,0 +1,23 @@ +import styled from "styled-components"; +import Tag from "./tag"; + +const StyledTagList = styled.div` + display: flex; + gap: 12px; + margin-top: 14px; + flex-wrap: wrap; +`; + +function TagList({ tags, onRemove }) { + return ( + + {tags.map((tag) => ( + onRemove(tag)}> + {"#" + tag} + + ))} + + ); +} + +export default TagList; diff --git a/vite-project/src/components/tag/tag.jsx b/vite-project/src/components/tag/tag.jsx new file mode 100644 index 00000000..bcf7813e --- /dev/null +++ b/vite-project/src/components/tag/tag.jsx @@ -0,0 +1,39 @@ +import styled from "styled-components"; +import removeImg from "../../assets/ic-xmark-fill.svg"; + +const StyledTag = styled.div` + background-color: var(--color-cool-gray-100); + color: var(--color-secondary-800); + font-size: 16px; + fontweight: 400; + line-height: 26px; + padding: 5px 0; + padding-left: 16px; + padding-right: 12px; + border-radius: 26px; + display: flex; + gap: 8px; + align-items: center; + + button { + border: none; + background: none; + cursor: pointer; + padding: 0; + width: 22px; + height: 24px; + } +`; + +function Tag({ children, onRemove }) { + return ( + + {children} + + + ); +} + +export default Tag; diff --git a/vite-project/src/components/text-input.jsx b/vite-project/src/components/text-input.jsx new file mode 100644 index 00000000..986e0316 --- /dev/null +++ b/vite-project/src/components/text-input.jsx @@ -0,0 +1,60 @@ +import styled, { css } from "styled-components"; + +const textStyle = css` + font-size: 16px; + font-weight: 400; + line-height: 26px; +`; + +const inputStyle = css` + ${textStyle} + width: 100%; + border: none; + background: none; + padding: 0; + color: var(--color-secondary-800); +`; + +const inputPlaceholderStyle = css` + color: var(--color-secondary-400); + ${textStyle} +`; + +const StyledTextInput = styled.div` + background-color: var(--color-secondary-100); + padding: 16px 24px; + border-radius: 12px; + ${({ $multiline }) => ($multiline ? "height: 282px;" : "")} + + input { + ${inputStyle} + } + input:focus { + outline: none; + } + input::placeholder { + ${inputPlaceholderStyle} + } + + textarea { + ${inputStyle} + height: 100%; + resize: none; + } + textarea:focus { + outline: none; + } + textarea::placeholder { + ${inputPlaceholderStyle} + } +`; + +function TextInput({ multiline = false, ...props }) { + return ( + + {multiline ?