Skip to content

Commit

Permalink
[최제원] sprint7 (#270)
Browse files Browse the repository at this point in the history
* Revert "[최제원] Sprint4 (#155)"

This reverts commit e46d37a.

* react npm start 오류로 인한 npm install 및 react 개발환경 셋팅

* header 추가 및 GET 리퀘스트를 받아 itemList 정렬

* 클래스 이름 추가 및 CSS 추가

* 파일 구조 정리 및 상품 정렬 쿼리 생성

* 최신순, 인기순 드롭아웃 html 구현과 검색 input 기능 html 구현

* form 태그 내부에 있는 태그들의 css 추가 및 수정

* 스프린트 미션5 미완성으로 코드잇에서 제공한 스프린트 미션5 완성본을 표본으로 커밋

* addItemPage html과 css 추가

* 이미지 삭제 기능 추가

* input 안에 들어가있는 value값 삭제

* sprint6 templete code copy and commit

* 상품 상세페이지 html 추가 및 api 연결과 간단한 css 작업도중 git push

* ItemDetailPage css 작업완료

* user information 추가 및 useNavigate를 이용하여 목록으로 돌아가기 기능 구현
  • Loading branch information
CJewon authored Dec 16, 2024
1 parent 8f45f50 commit d95cacb
Show file tree
Hide file tree
Showing 30 changed files with 16,409 additions and 1,817 deletions.
17,002 changes: 15,386 additions & 1,616 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "1-weekly-mission",
"name": "panda-market-react",
"version": "0.1.0",
"private": true,
"dependencies": {
Expand All @@ -8,8 +8,9 @@
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^7.0.1",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"styled-components": "^6.1.13",
"web-vitals": "^2.1.4"
},
"scripts": {
Expand Down
3 changes: 2 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import MarketPage from "./pages/MarketPage/MarketPage";
import AddItemPage from "./pages/AddItemPage/AddItemPage";
import CommunityFeedPage from "./pages/CommunityFeedPage/CommunityFeedPage";
import Header from "./components/Layout/Header";

import ItemDetailPage from "./pages/ItemDetailPage/ItemDetailPage";
function App() {
return (
<BrowserRouter>
Expand All @@ -20,6 +20,7 @@ function App() {
<Route path="items" element={<MarketPage />} />
<Route path="additem" element={<AddItemPage />} />
<Route path="community" element={<CommunityFeedPage />} />
<Route path="items/:productId" element={<ItemDetailPage />}></Route>
</Routes>
</div>
</BrowserRouter>
Expand Down
32 changes: 32 additions & 0 deletions src/api/itemApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,35 @@ export async function getProducts(params = {}) {
throw error;
}
}

export async function getProductsById(productId) {
try {
const response = await fetch(
`https://panda-market-api.vercel.app/products/${productId}`
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const body = await response.json();
return body;
} catch (error) {
console.error("Failed to fetch product by ID:", error);
throw error;
}
}

export async function getProductsComments({ productId, limit = 5 }) {
try {
const response = await fetch(
`https://panda-market-api.vercel.app/products/${productId}/comments?limit=${limit}`
);
if (!response.ok) {
throw new Error(`HTTP error : ${response.status}`);
}
const body = await response.json();
return body;
} catch (error) {
console.error("Failed to fetch product by comments", error);
throw error;
}
}
Binary file added src/assets/images/icons/Img_inquiry_empty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/icons/ic_back.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/icons/ic_heart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/icons/ic_kebab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/images/icons/ic_plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/icons/ic_profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/images/icons/ic_x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 13 additions & 2 deletions src/components/Layout/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import Logo from "../../assets/images/logo/logo.svg";
import { Link, NavLink } from "react-router-dom";
import { Link, NavLink, useLocation } from "react-router-dom";
import "./Header.css";

// react-router-dom의 NavLink를 이용하면 활성화된 네비게이션 항목을 하이라이트해줄 수 있어요!
Expand All @@ -9,6 +9,8 @@ function getLinkStyle({ isActive }) {
}

function Header() {
const location = useLocation(); // 현재 경로 정보

return (
<header className="globalHeader">
<div className="headerLeft">
Expand All @@ -24,7 +26,16 @@ function Header() {
</NavLink>
</li>
<li>
<NavLink to="/items" style={getLinkStyle}>
{/* React Router v6 이전 버전에서는 NavLink `isActive` prop으로 바로 스타일 정보를 넣어줄 수 있었지만, 최신 버전에서는 className 또는 style을 이용해야 해요 */}
{/* /additem 페이지에서도 네이게이션의 '중고마켓' 링크 하이라이트 */}
<NavLink
to="/items"
style={({ isActive }) =>
location.pathname === "/additem" || isActive
? { color: "var(--blue)" }
: {}
}
>
중고마켓
</NavLink>
</li>
Expand Down
27 changes: 27 additions & 0 deletions src/components/UI/DeleteButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
import styled from "styled-components";
import { ReactComponent as CloseIcon } from "../../assets/images/icons/ic_x.svg";

const Button = styled.button`
background-color: ${({ theme }) => theme.colors.gray[0]};
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
&:hover {
background-color: ${({ theme }) => theme.colors.blue[0]};
}
`;

function DeleteButton({ onClick, label }) {
return (
<Button aria-label={`${label} 삭제`} onClick={onClick}>
<CloseIcon />
</Button>
);
}

export default DeleteButton;
33 changes: 33 additions & 0 deletions src/components/UI/DropdownMenu.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.sortButtonWrapper {
position: relative;
}

.sortDropdownTriggerButton {
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 9px;
margin-left: 8px;
}

.dropdownMenu {
position: absolute;
top: 110%;
right: 0;
background: #fff;
border-radius: 8px;
border: 1px solid #e5e7eb;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 99;
}

.dropdownItem {
padding: 12px 44px;
border-bottom: 1px solid #e5e7eb;
font-size: 16px;
color: #1f2937;
cursor: pointer;
}

.dropdownItem:last-child {
border-bottom: none;
}
43 changes: 43 additions & 0 deletions src/components/UI/DropdownMenu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useState } from "react";
import "./DropdownMenu.css";
import { ReactComponent as SortIcon } from "../../assets/images/icons/ic_sort.svg";

function DropdownMenu({ onSortSelection }) {
const [isDropdownVisible, setIsDropdownVisible] = useState(false);

const toggleDropdown = () => {
setIsDropdownVisible(!isDropdownVisible);
};

return (
<div className="sortButtonWrapper">
<button className="sortDropdownTriggerButton" onClick={toggleDropdown}>
<SortIcon />
</button>

{isDropdownVisible && (
<div className="dropdownMenu">
<div
className="dropdownItem"
onClick={() => {
onSortSelection("recent");
setIsDropdownVisible(false);
}}
>
최신순
</div>
<div
className="dropdownItem"
onClick={() => {
onSortSelection("favorite");
setIsDropdownVisible(false);
}}
>
인기순
</div>
</div>
)}
</div>
);
}
export default DropdownMenu;
122 changes: 122 additions & 0 deletions src/components/UI/ImageUpload.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React, { useState } from "react";
import { Label } from "./InputItem";
import styled, { css } from "styled-components";
import { ReactComponent as PlusIcon } from "../../assets/images/icons/ic_plus.svg";
import DeleteButton from "./DeleteButton";

const ImageUploadContainer = styled.div`
display: flex;
gap: 8px;
@media ${({ theme }) => theme.mediaQuery.tablet} {
gap: 18px;
}
@media ${({ theme }) => theme.mediaQuery.desktop} {
gap: 24px;
}
`;

const squareStyles = css`
// 작은 화면에서는 max-width가 되기 전까지는 UploadButton과 ImagePreview가 각각 gap을 포함해 컨테이너 너비의 절반을 차지하도록 함
width: calc(50% - 4px);
max-width: 200px;
aspect-ratio: 1 / 1; // 정사각형 비율 유지
border-radius: 12px;
@media ${({ theme }) => theme.mediaQuery.tablet} {
width: 162px;
}
@media ${({ theme }) => theme.mediaQuery.desktop} {
width: 282px;
}
`;

// file input과 연관 짓기 위해 버튼이 대신 label로 설정
const UploadButton = styled.label`
background-color: ${({ theme }) => theme.colors.gray[1]};
color: ${({ theme }) => theme.colors.gray[0]};
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
font-size: 16px;
cursor: pointer; // 버튼이 아닌 label을 사용한 경우 별도로 추가해 주세요
&:hover {
background-color: ${({ theme }) => theme.colors.gray[2]};
}
${squareStyles}
`;

const ImagePreview = styled.div`
background-image: url(${({ src }) => src});
background-size: cover;
background-position: center;
position: relative; // DeleteButton 포지셔닝을 위해 추가
${squareStyles}
`;

const DeleteButtonWrapper = styled.div`
position: absolute;
top: 12px;
right: 12px;
`;

// 브라우저 기본 '파일 선택' 버튼 대신 커스텀 버튼을 사용하기 위해 file input을 숨김 처리
const HiddenFileInput = styled.input`
display: none;
`;

function ImageUpload({ title }) {
const [imagePreviewUrl, setImagePreviewUrl] = useState("");

const handleImageChange = (event) => {
const file = event.target.files[0];
if (file) {
// 미리보기 주소 값(Object URL) 생성
const imageUrl = URL.createObjectURL(file);
setImagePreviewUrl(imageUrl);
}
};

const handleDelete = () => {
setImagePreviewUrl(""); // 미리보기 URL 리셋
};

return (
<div>
{title && <Label>{title}</Label>}

<ImageUploadContainer>
{/* HiddenFileInput의 id와 label의 htmlFor 값을 매칭해 주세요 */}
<UploadButton htmlFor="image-upload">
<PlusIcon />
이미지 등록
</UploadButton>

<HiddenFileInput
id="image-upload"
type="file"
onChange={handleImageChange}
accept="image/*" // 이미지 파일만 업로드 가능하도록 제한
/>

{/* 업로드된 이미지가 있으면 썸네일 렌더링 */}
{imagePreviewUrl && (
<ImagePreview src={imagePreviewUrl}>
<DeleteButtonWrapper>
<DeleteButton onClick={handleDelete} label="이미지 파일" />
</DeleteButtonWrapper>
</ImagePreview>
)}
</ImageUploadContainer>
</div>
);
}

export default ImageUpload;
Loading

0 comments on commit d95cacb

Please sign in to comment.