Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ca966ea
feat: GlobalStyle로 기본 글자색 설정
MyungJiwoo May 8, 2025
8ad5c18
feat: 상품 추가 페이지에서 navbar의 '중고마켓' 항목 활성화
MyungJiwoo May 8, 2025
b8c4601
feat: useDebounce 커스텀 훅 추가
MyungJiwoo May 8, 2025
b2e3a9c
chore: 필요한 아이콘 추가
MyungJiwoo May 8, 2025
cda5ff5
feat: 상품 추가 페이지 구현
MyungJiwoo May 8, 2025
38c9952
docs: 요구사항 목록 정리 및 진행 상황 체크
MyungJiwoo May 8, 2025
2f6b8db
chore: not found image 변경
MyungJiwoo May 10, 2025
02978e3
feat: 토스트 메시지 추가 및 설정
MyungJiwoo May 10, 2025
7132404
refactor: 전역 레이아웃 수정
MyungJiwoo May 10, 2025
03f6823
refactor: 상품명 input을 BaseInput, InputField, ItemNameInputField로 분리
MyungJiwoo May 10, 2025
19b21ac
refactor: 상품 소개 textarea를 BaseTextarea, TextareaField, ItemDescriptio…
MyungJiwoo May 10, 2025
a941fb7
refactor: 판매 가격 input을 ItemPriceInputField로 분리
MyungJiwoo May 10, 2025
24ba981
refactor: 상품 이미지 input을 BaseImageInput, ImageInputField, ItemImageInp…
MyungJiwoo May 10, 2025
9fa48b0
refactor: 태그 input을 ItemTagInputField, TagsSection으로 분리
MyungJiwoo May 10, 2025
4b7ce2c
refactor: HeaderSection 분리
MyungJiwoo May 10, 2025
355327b
refactor: 재사용 Form 컴포넌트 생성
MyungJiwoo May 10, 2025
a6fa9d0
refactor: 재사용 컴포넌트로 리팩토링
MyungJiwoo May 10, 2025
183b67c
fix: BaseForm에 누락된 props 추가
MyungJiwoo May 10, 2025
8201084
refactor: item image 관련 로직은 useImageHandler로 분리
MyungJiwoo May 10, 2025
7e99c77
refactor: 제출 버튼 활성화를 위한 Form 유효성 검사를 useFormValidation 훅으로 분리
MyungJiwoo May 10, 2025
bf6cb57
refactor: Form 제출 관련 로직을 useSubmitHandler 훅으로 분리
MyungJiwoo May 10, 2025
8836b97
refactor: 태그 관련 비즈니스 로직을 useTagHandler 훅으로 분리
MyungJiwoo May 10, 2025
95d8bb6
refactor: 분리한 커스텀 훅 적용
MyungJiwoo May 10, 2025
8efbf46
refactor: 삭제 버튼을 DeleteButton 공통 컴포넌트로 분리
MyungJiwoo May 10, 2025
8487f4d
docs: README에 추가 기능 및 구현 포인트 추가
MyungJiwoo May 10, 2025
28b5042
docs: README 문구 수정
MyungJiwoo May 10, 2025
89fe891
docs: README 문구 수정 2
MyungJiwoo May 10, 2025
9ecb81c
feat: 토스트 메시지로 이미지 등록의 최대 개수 안내
MyungJiwoo May 10, 2025
a4c4eef
fix: textarea 폰트 크기 설정
MyungJiwoo May 10, 2025
3bd4fb1
refactor: 모바일의 전역 레이아웃에 좌우 여백 추가
MyungJiwoo May 10, 2025
273218d
fix: 모바일에서 상품 등록 페이지로 이동하지 않는 오류 수정
MyungJiwoo May 10, 2025
fbd931b
fix: 화면 크기에 따라 이미지를 추가하면 1:1 비율이 달라지는 오류 수정
MyungJiwoo May 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 17 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## 판다마켓 5
## 판다마켓 6

**🌐 배포 url: https://myungjiwoo-pandamarket.netlify.app/items**
**🌐 배포 url: https://myungjiwoo-pandamarket.netlify.app/additem**

### 기본 요구사항

Expand All @@ -10,35 +10,24 @@

### 체크 리스트 (기본)

- [x] 중고마켓 페이지 주소는 "/items"이다.
- [x] 페이지 주소가 "/items"일 때 상단 네비게이션바의 "중고마켓" 버튼의 색상은 "3692FF"이다.
- [x] 상단 네비게이션 바는 이전 미션에서 구현한 랜딩 페이지와 동일한 스타일로 제작한다.
- [x] 상품 등록 페이지 주소는 "/additem"이다.
- [x] 페이지 주소가 "/additem"일 때 상단 네비게이션바의 "중고마켓" 버튼의 색상은 "3692FF"이다.
- [x] 상품 이미지는 최대 한 개까지 업로드할 수 있다.
- [x] 각 input의 placeholder 값을 정확히 입력한다.
- [x] 이미지를 제외하고 input에 모든 값을 입력하면 '등록' 버튼이 활성화 된다. (api를 통한 상품 등록은 추후 미션에서 적용)

**데이터 불러오기**

- [x] 카드 데이터는 제공된 백엔드 API 페이지의 GET 메소드인 "/products"를 사용한다.

**상품 정렬**
### 체크 리스트 (심화)

- [x] 전체 상품에서 드롭 다운으로 "최신 순" 또는 "좋아요 순"을 선택해서 정렬할 수 있다.
- [x] "상품 등록하기" 버튼을 누르면 "/additem"으로 이동한다. (빈 페이지)
- [x] 베스트 상품 기준
- 정렬 : favorite
- favorite가 가장 높은 상품 4가지
- [x] 이미지 안의 x 버튼을 누르면 이미지가 삭제된다.
- [x] 추가된 태그 안의 x 버튼을 누르면 해당 태그는 삭제된다.

**반응형 디자인**
### 추가 기능

- [x] 미디어 쿼리를 사용해서 반응형 view마다 물품의 개수를 다르게 보여준다. (서버로 요청하는 값은 동일)
- [x] 베스트 상품
- Desktop : 4개 보이기
- Tablet : 2개 보이기
- Mobile : 1개 보이기
- [x] 전체 상품
- Desktop : 10개 보이기
- Tablet : 6개 보이기
- Mobile : 4개 보이기
- [x] 오류 메시지를 토스트 메시지로 구현 (react-toastify 라이브러리 사용)

### 체크 리스트 (심화)
### 구현 포인트

- [x] 페이지네이션 기능을 구현한다.
- [x] 반응형으로 보여지는 물품들의 개수를 다르게 설정할 때 서버에 보내는 pageSize 값을 적절하게 설정한다.
- [x] 입력 컴포넌트 계층화 및 재사용
- `Base~ 컴포넌트` : 최소 단위 입력 컴포넌트
- `~Field 컴포넌트` : 공통 인터페이스를 추가한 확장 컴포넌트 (label, error messge 등)
- `Item~Field 컴포넌트` : 도메인 전용 컴포넌트
25 changes: 24 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"axios": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.5.0"
"react-router-dom": "^7.5.0",
"react-toastify": "^11.0.5"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
Expand Down
18 changes: 17 additions & 1 deletion src/App.jsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💊 제안
최상단에서 useDeviceSize 훅을 호출해 isMobile가 변경되게 되면 모든 자식컴포넌트들이 리렌더링됩니다.
Toast 라이브러리를 위해 이렇게 처리하신 것 같은데 제가 문서를 봤을 때는 다른 방식으로도
원하시는 바를 구현하실 수 있을 것 같아요~

https://fkhadra.github.io/react-toastify/how-to-style

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import styled from "@emotion/styled";
import GlobalStyle from "./GlobalStyle";
import Header from "@/layouts/Header";
import { breakpoints } from "@constants/breakpoints";
import useDeviceSize from "@hooks/useDeviceSize";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

const Layout = styled.div`
display: flex;
Expand All @@ -13,7 +16,8 @@ const Layout = styled.div`

const ResponsiveMain = styled.main`
// 모바일 & 전체
width: 95vw;
width: 90vw;
margin: 2.5rem 0;

// 태블릿
@media (min-width: ${breakpoints.mobile}) {
Expand All @@ -27,13 +31,25 @@ const ResponsiveMain = styled.main`
`;

const App = () => {
const { isMobile } = useDeviceSize();

return (
<Layout>
<GlobalStyle />

<Header />
<ResponsiveMain>
<Outlet />
</ResponsiveMain>

<ToastContainer
toastClassName="custom-toast"
progressClassName="custom-progress-bar"
position={isMobile ? "top-center" : "bottom-right"}
style={isMobile && { marginTop: "7rem" }}
autoClose={3000}
limit={4}
/>
</Layout>
);
};
Expand Down
22 changes: 22 additions & 0 deletions src/GlobalStyle.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { Global, css } from "@emotion/react";
import { breakpoints } from "@constants/breakpoints";

const baseStyle = css`
:root {
Expand Down Expand Up @@ -31,6 +32,7 @@ const baseStyle = css`
background-color: var(#fcfcfc);
font-family: "Pretendard", sans-serif;
font-size: 10px;
color: var(--gray800);
}

div,
Expand All @@ -47,6 +49,26 @@ const baseStyle = css`
button {
cursor: pointer;
}

// 토스트 메시지 스타일
.custom-toast {
background-color: var(--white);
font-size: 1.4rem;
border-radius: 1rem;
overflow: hidden;
}

.custom-progress-bar {
background: linear-gradient(to left, #00c6ff, #0072ff);
}

// 모바일 토스트 메시지 스타일
@media (max-width: ${breakpoints.tablet}) {
.custom-toast {
margin: 0.3rem 0;
width: 70vw;
}
}
`;

const GlobalStyle = () => <Global styles={baseStyle} />;
Expand Down
26 changes: 26 additions & 0 deletions src/assets/icons/delete.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const DeleteIcon = () => {
return (
<svg
width="8"
height="8"
viewBox="0 0 10 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.08044 1L9.08044 9"
stroke="white"
strokeWidth="1.8"
strokeLinecap="round"
/>
<path
d="M8.99994 1L0.99994 9"
stroke="white"
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
);
};

export default DeleteIcon;
26 changes: 26 additions & 0 deletions src/assets/icons/plus.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const PlusIcon = () => {
return (
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 24H38"
stroke="#9CA3AF"
strokeWidth="4"
strokeLinecap="round"
/>
<path
d="M24 38V10"
stroke="#9CA3AF"
strokeWidth="4"
strokeLinecap="round"
/>
</svg>
);
};

export default PlusIcon;
Binary file modified src/assets/imgs/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/components/BaseForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const BaseForm = ({ children, onSubmit, ...props }) => {
return (
<form onSubmit={onSubmit} {...props}>
{children}
</form>
);
};

export default BaseForm;
Comment on lines +1 to +9
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ 질문
이렇게 form을 공통 컴포넌트로 빼야하는 이유가 있을까요?
단순히 form의 이름을 바꾸는 느낌이라 그냥 사용해도 될 것 같아요~
만약 의도가 있으시거나 추후 기능을 추가하실 생각이면 두셔도 좋습니다~

11 changes: 11 additions & 0 deletions src/components/BaseImageInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import styled from "@emotion/styled";

const BaseImageInput = ({ type = "file", onChange, ...props }) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💊 제안
BaseInput이라는 이름은 꼭 다른 image input이 있을것처럼 느껴지게 되네요. 다른 input이 없다면 그냥 ImageInput이여도 될 것 같아요!

return <Input type={type} accept="image/*" onChange={onChange} {...props} />;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💊 제안
input의 accept 속성은 유저가 어떤 파일을 올려야하는지에 대한 힌트를 제공하는 속성입니다.
유저는 파일 업로드시 accept의 명시된 확장자 이외의 파일도 올릴 수 있으므로
실제 upload 함수에서 한번더 확장자를 검사해주시는 것이 좋습니다.

(사용자가 업로드창에서 옵션을 열어 확장자를 바꾸면 아래처럼 보입니다)
스크린샷 2025-05-08 오후 5 53 17

https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/accept

};

export default BaseImageInput;

const Input = styled.input`
display: none;
`;
21 changes: 21 additions & 0 deletions src/components/BaseInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import styled from "@emotion/styled";

const BaseInput = ({ type = "text", value, onChange, ...props }) => {
return <Input type={type} value={value} onChange={onChange} {...props} />;
};

export default BaseInput;

const Input = styled.input`
width: 100%;
padding: 1.4rem 2.4rem;
background-color: var(--gray100);
border-radius: 1.2rem;
border: 2px solid var(--gray100);
font-size: 1.4rem;

&:focus {
outline: none;
border: 2px solid var(--blue);
}
`;
25 changes: 25 additions & 0 deletions src/components/BaseTextarea.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import styled from "@emotion/styled";

const BaseTextarea = ({ ref, value, onChange, ...props }) => {
return <Textarea ref={ref} value={value} onChange={onChange} {...props} />;
};

export default BaseTextarea;

const Textarea = styled.textarea`
width: 100%;
height: auto;
min-height: 8rem;
resize: none;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 칭찬
resize 막아두신 점 좋아요~

overflow: hidden;
padding: 1.6rem 2.4rem;
background-color: var(--gray100);
border-radius: 1.2rem;
border: 2px solid var(--gray100);
font-size: 1.4rem;

&:focus {
outline: none;
border: 2px solid var(--blue);
}
`;
34 changes: 34 additions & 0 deletions src/components/DeleteButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import styled from "@emotion/styled";
import DeleteIcon from "@assets/icons/delete";

const BUTTON_SIZE = {
s: "1.6rem",
m: "2rem",
l: "2.4rem",
};

const DeleteButton = ({ onClick, size = "s" }) => {
return (
<DeleteButtonWrapper onClick={onClick} size={BUTTON_SIZE[size]}>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❗️ 수정요청
이런 클릭 가능한 요소의 경우 버튼을 사용해주세요. 더 적절한 태그가 있는데 div 태그에 onClick 을 주게되면 button에게 기대하는 동작을 제대로 수행하지 못합니다!

Suggested change
<DeleteButtonWrapper onClick={onClick} size={BUTTON_SIZE[size]}>
<DeleteButtonWrapper type="button" onClick={onClick} size={BUTTON_SIZE[size]}>

버튼으로 바꿔주시고 type을 명시하시는 것을 추천드려요~

<DeleteIcon />
</DeleteButtonWrapper>
);
};

export default DeleteButton;

const DeleteButtonWrapper = styled.div`
width: ${(props) => props.size};
height: ${(props) => props.size};

display: flex;
justify-content: center;
align-items: center;
border-radius: 100%;
background-color: var(--gray300);
cursor: pointer;

&:hover {
opacity: 0.7;
}
`;
Loading
Loading