diff --git a/vite-project/.env.development b/vite-project/.env.development deleted file mode 100644 index 887a088a..00000000 --- a/vite-project/.env.development +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 887a088a..00000000 --- a/vite-project/.env.production +++ /dev/null @@ -1 +0,0 @@ -VITE_API_BASE_URL=https://panda-market-api.vercel.app \ No newline at end of file diff --git a/vite-project/.gitignore b/vite-project/.gitignore index a547bf36..a3ab6e36 100644 --- a/vite-project/.gitignore +++ b/vite-project/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env.* \ No newline at end of file diff --git a/vite-project/src/api/api.js b/vite-project/src/api/api.js new file mode 100644 index 00000000..1dc0828f --- /dev/null +++ b/vite-project/src/api/api.js @@ -0,0 +1,22 @@ +function trimPath(str) { + let trimmed = str.trim(); + while (trimmed.startsWith("/")) { + trimmed = trimmed.slice(1); + } + return trimmed; +} + +export function createUrl(path, params) { + const trimmed = trimPath(path); + const url = new URL(`${import.meta.env.VITE_API_BASE_URL}/${trimmed}`); + + if (!params) { + return url; + } + + Object.keys(params).forEach((key) => + url.searchParams.append(key, params[key]) + ); + + return url; +} diff --git a/vite-project/src/api/comments-mock.json b/vite-project/src/api/comments-mock.json new file mode 100644 index 00000000..c2c22343 --- /dev/null +++ b/vite-project/src/api/comments-mock.json @@ -0,0 +1,60 @@ +{ + "nextCursor": 0, + "list": [ + { + "writer": { + "image": "", + "nickname": "똑똑한 판다", + "id": 1 + }, + "updatedAt": "2024-08-06T02:57:22.000Z", + "createdAt": "2024-08-06T02:57:22.000Z", + "content": "혹시 사용 기간이 어떻게 되실까요?", + "id": 1 + }, + { + "writer": { + "image": "", + "nickname": "똑똑한 고양이", + "id": 2 + }, + "updatedAt": "2025-07-06T02:57:22.000Z", + "createdAt": "2025-07-06T02:57:22.000Z", + "content": "잘 작동하나요??", + "id": 2 + }, + { + "writer": { + "image": "", + "nickname": "똑똑한 강아지", + "id": 3 + }, + "updatedAt": "2025-08-03T02:57:22.000Z", + "createdAt": "2025-08-03T02:57:22.000Z", + "content": "저도 사고싶어요!", + "id": 3 + }, + { + "writer": { + "image": "", + "nickname": "똑똑한 고양이", + "id": 2 + }, + "updatedAt": "2025-08-06T02:57:22.000Z", + "createdAt": "2025-08-06T02:57:22.000Z", + "content": "제가 샀습니다.", + "id": 4 + }, + { + "writer": { + "image": "", + "nickname": "똑똑한 토끼", + "id": 4 + }, + "updatedAt": "2025-08-06T07:10:22.000Z", + "createdAt": "2025-08-06T07:10:22.000Z", + "content": "까비...", + "id": 5 + } + ] +} diff --git a/vite-project/src/api/comments.js b/vite-project/src/api/comments.js new file mode 100644 index 00000000..c7114311 --- /dev/null +++ b/vite-project/src/api/comments.js @@ -0,0 +1,15 @@ +import mock from "./comments-mock.json"; + +export async function fetchComments(productId) { + // const url = createUrl(`products/${productId}/comments`, { limit }); + // const response = await fetch(url); + // if (!response.ok) { + // throw new Error("Failed to fetch comments"); + // } + + // const json = await response.json(); + // return json.list; + + if (!productId) return; + return mock.list; +} diff --git a/vite-project/src/api/products.js b/vite-project/src/api/products.js index fe32ac72..b73847af 100644 --- a/vite-project/src/api/products.js +++ b/vite-project/src/api/products.js @@ -1,19 +1,23 @@ +import { createUrl } from "./api"; + export async function fetchProducts({ keyword = "", page = 1, pageSize = 10, orderBy = "recent", } = {}) { - 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); + const params = { + page, + pageSize, + orderBy, + }; + if (keyword) { - url.searchParams.append("keyword", keyword); + params.keyword = keyword; } + const url = createUrl("products", params); const response = await fetch(url); - if (!response.ok) { throw new Error("Failed to fetch products"); } @@ -24,3 +28,14 @@ export async function fetchProducts({ numberOfPages: Math.ceil(json.totalCount / pageSize), }; } + +export async function fetchProduct(id) { + const url = createUrl(`products/${id}`); + const response = await fetch(url); + if (!response.ok) { + throw new Error("Failed to fetch product"); + } + + const json = await response.json(); + return json; +} diff --git a/vite-project/src/assets/comments-empty.png b/vite-project/src/assets/comments-empty.png new file mode 100644 index 00000000..25019b5a Binary files /dev/null and b/vite-project/src/assets/comments-empty.png differ diff --git a/vite-project/src/assets/ic-arrow-back.svg b/vite-project/src/assets/ic-arrow-back.svg new file mode 100644 index 00000000..9ba5ad94 --- /dev/null +++ b/vite-project/src/assets/ic-arrow-back.svg @@ -0,0 +1,4 @@ + + + + diff --git a/vite-project/src/assets/ic-dots-3-vertical.svg b/vite-project/src/assets/ic-dots-3-vertical.svg new file mode 100644 index 00000000..dd7ed7f5 --- /dev/null +++ b/vite-project/src/assets/ic-dots-3-vertical.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/vite-project/src/components/Button.jsx b/vite-project/src/components/Button.jsx deleted file mode 100644 index 4019d336..00000000 --- a/vite-project/src/components/Button.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import styled from "styled-components"; - -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/Nav.jsx b/vite-project/src/components/Nav.jsx index ed76fa9c..a95cbe47 100644 --- a/vite-project/src/components/Nav.jsx +++ b/vite-project/src/components/Nav.jsx @@ -1,9 +1,8 @@ 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"; +import Avatar from "./avatar"; function NavigationLink({ to, activePaths = [], children }) { const location = useLocation(); @@ -40,9 +39,7 @@ function Nav() { 중고마켓 -
- -
+ ); diff --git a/vite-project/src/components/OrderBySelect.css b/vite-project/src/components/OrderBySelect.css index 8c006ce8..42f4f3a2 100644 --- a/vite-project/src/components/OrderBySelect.css +++ b/vite-project/src/components/OrderBySelect.css @@ -16,7 +16,7 @@ .OrderBySelect { background-color: white; - padding: 12px 20px; + padding: 8px 20px; display: flex; gap: 24px; outline: none; diff --git a/vite-project/src/components/SearchInput.css b/vite-project/src/components/SearchInput.css deleted file mode 100644 index bf0b8a14..00000000 --- a/vite-project/src/components/SearchInput.css +++ /dev/null @@ -1,47 +0,0 @@ -@import url("../palette.css"); - -.SearchInput { - background-color: var(--color-secondary-100); - padding: 9px 16px; - display: flex; - gap: 4px; - border-radius: 12px; - width: 325px; -} - -.SearchInput img { - width: 24px; -} - -.SearchInput input { - border: none; - background: none; - font-size: 16px; - font-weight: 400; - line-height: 26px; - flex-grow: 1; -} -.SearchInput input:focus { - outline: none; -} -.SearchInput input::placeholder { - color: var(--color-secondary-400); - font-size: 16px; - font-weight: 400; - line-height: 26px; -} - -/* Tablet */ -@media (max-width: 1199px) { - .SearchInput { - width: 242px; - } -} - -/* Mobile */ -@media (max-width: 767px) { - .SearchInput { - width: 100%; - flex-grow: 1; - } -} diff --git a/vite-project/src/components/SearchInput.jsx b/vite-project/src/components/SearchInput.jsx deleted file mode 100644 index 8f0e0b6d..00000000 --- a/vite-project/src/components/SearchInput.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import searchImg from "../assets/ic-magnifier.svg"; -import "./SearchInput.css"; - -function SearchInput({ placeholder }) { - return ( -
- 상품 검색 - -
- ); -} - -export default SearchInput; diff --git a/vite-project/src/components/add-item/adding-item-image.jsx b/vite-project/src/components/add-item/adding-item-image.jsx index 8c4f3b8c..20729d78 100644 --- a/vite-project/src/components/add-item/adding-item-image.jsx +++ b/vite-project/src/components/add-item/adding-item-image.jsx @@ -3,11 +3,12 @@ import removeImg from "../../assets/ic-xmark-fill.svg"; const StyledAddingItemImage = styled.div` width: 282px; + height: 282px; position: relative; & > img { width: 100%; - aspect-ratio: 1; + height: 100%; object-fit: cover; border-radius: 12px; } @@ -25,10 +26,12 @@ const StyledAddingItemImage = styled.div` @media (max-width: 1199px) { width: 168px; + height: 168px; } @media (max-width: 767px) { width: 50%; + height: auto; } `; diff --git a/vite-project/src/components/avatar.jsx b/vite-project/src/components/avatar.jsx new file mode 100644 index 00000000..c8773a6a --- /dev/null +++ b/vite-project/src/components/avatar.jsx @@ -0,0 +1,26 @@ +import styled from "styled-components"; +import defaultImg from "../assets/profile-default.svg"; + +const StyledAvatar = styled.div` + width: ${({ $size }) => $size}px; + height: ${({ $size }) => $size}px; + + img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 20; + } +`; + +const DEFAULT_SIZE = 40; + +function Avatar({ imageUrl, size = DEFAULT_SIZE }) { + return ( + + 프로필 사진 + + ); +} + +export default Avatar; diff --git a/vite-project/src/components/button/button-styles.js b/vite-project/src/components/button/button-styles.js new file mode 100644 index 00000000..29da424a --- /dev/null +++ b/vite-project/src/components/button/button-styles.js @@ -0,0 +1,9 @@ +export const BUTTON_SIZE = { + medium: "medium", + small: "small", +}; + +export const BUTTON_TYPE = { + round: "round", + pill: "pill", +}; diff --git a/vite-project/src/components/button/button.jsx b/vite-project/src/components/button/button.jsx new file mode 100644 index 00000000..18602a68 --- /dev/null +++ b/vite-project/src/components/button/button.jsx @@ -0,0 +1,52 @@ +import styled from "styled-components"; +import { BUTTON_SIZE, BUTTON_TYPE } from "./button-styles"; + +function padding(buttonSize) { + return buttonSize === BUTTON_SIZE.medium ? "11px 40px" : "8px 23px"; +} + +function fontSize(buttonSize) { + return buttonSize === BUTTON_SIZE.medium ? 18 : 16; +} + +function borderRadius(buttonType) { + return buttonType === BUTTON_TYPE.round ? 8 : 24; +} + +const StyledButton = styled.button` + display: flex; + gap: 8px; + background-color: var(--color-primary-100); + padding: ${({ $size }) => padding($size)}; + color: var(--color-cool-gray-100); + font-size: ${({ $size }) => fontSize($size)}px; + font-weight: 600; + line-height: 26px; + border-radius: ${({ $type }) => borderRadius($type)}px; + border: none; + cursor: pointer; + + &:disabled { + background-color: var(--color-cool-gray-400); + cursor: default; + } + + &:hover:not(:disabled) { + opacity: 0.5; + } +`; + +function Button({ + children, + size = BUTTON_SIZE.small, + type = BUTTON_TYPE.round, + ...props +}) { + return ( + + {children} + + ); +} + +export default Button; diff --git a/vite-project/src/components/button/icon-button.jsx b/vite-project/src/components/button/icon-button.jsx new file mode 100644 index 00000000..71d1af15 --- /dev/null +++ b/vite-project/src/components/button/icon-button.jsx @@ -0,0 +1,25 @@ +import styled from "styled-components"; + +const StyledIconButton = styled.button` + width: ${({ $size }) => $size}px; + height: ${({ $size }) => $size}px; + background: none; + border: none; + padding: 0; + cursor: pointer; + + img { + width: 100%; + height: 100%; + } +`; + +function IconButton({ className, src, size = 24, onClick }) { + return ( + + + + ); +} + +export default IconButton; diff --git a/vite-project/src/components/favorite-button.jsx b/vite-project/src/components/favorite-button.jsx new file mode 100644 index 00000000..7c1f2290 --- /dev/null +++ b/vite-project/src/components/favorite-button.jsx @@ -0,0 +1,42 @@ +import styled from "styled-components"; +import favoriteImg from "../assets/ic-heart.svg"; + +const StyledFavoriteButton = styled.div` + display: flex; + gap: 4px; + align-items: center; + border: 1px solid var(--color-secondary-200); + border-radius: 40px; + background: none; + padding: 4px 12px; + + img { + width: 32px; + height: 32px; + } + + span { + font-size: 16px; + font-weight: 500; + line-height: 26px; + color: var(--color-cool-gray-500); + } + + @media (max-width: 1199px) { + div { + width: 24px; + height: 24px; + } + } +`; + +function FavoriteButton({ count, isFavorite }) { + return ( + + 좋아요 + {count} + + ); +} + +export default FavoriteButton; diff --git a/vite-project/src/components/input/search-input.jsx b/vite-project/src/components/input/search-input.jsx new file mode 100644 index 00000000..58963e11 --- /dev/null +++ b/vite-project/src/components/input/search-input.jsx @@ -0,0 +1,53 @@ +import styled from "styled-components"; +import searchImg from "../../assets/ic-magnifier.svg"; + +const StyledSearchInput = styled.div` + background-color: var(--color-secondary-100); + padding: 9px 16px; + display: flex; + gap: 4px; + border-radius: 12px; + width: 325px; + + img { + width: 24px; + } + + input { + border: none; + background: none; + font-size: 16px; + font-weight: 400; + line-height: 26px; + flex-grow: 1; + } + input:focus { + outline: none; + } + input::placeholder { + color: var(--color-secondary-400); + font-size: 16px; + font-weight: 400; + line-height: 26px; + } + + @media (max-width: 1199px) { + width: 242px; + } + + @media (max-width: 767px) { + width: 100%; + flex-grow: 1; + } +`; + +function SearchInput({ placeholder }) { + return ( + + 상품 검색 + + + ); +} + +export default SearchInput; diff --git a/vite-project/src/components/text-input.jsx b/vite-project/src/components/input/text-input.jsx similarity index 90% rename from vite-project/src/components/text-input.jsx rename to vite-project/src/components/input/text-input.jsx index 986e0316..8a3c79c2 100644 --- a/vite-project/src/components/text-input.jsx +++ b/vite-project/src/components/input/text-input.jsx @@ -24,7 +24,6 @@ const StyledTextInput = styled.div` background-color: var(--color-secondary-100); padding: 16px 24px; border-radius: 12px; - ${({ $multiline }) => ($multiline ? "height: 282px;" : "")} input { ${inputStyle} @@ -51,7 +50,7 @@ const StyledTextInput = styled.div` function TextInput({ multiline = false, ...props }) { return ( - + {multiline ?