Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*

#환경변수 무시
.env
Comment on lines +25 to +26
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으 ~ 굿굿 👍

환경변수를 만들고 gitignore에 추가하셨군요 ! 😊
피드백 반영 넘 좋습니당

253 changes: 202 additions & 51 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.7.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3",
Expand Down
42 changes: 9 additions & 33 deletions src/api/itemApi.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,16 @@
//itemApi.js
const BASE_URL = process.env.REACT_APP_BASE_URL;

// export async function getProducts(params = {}) {
// // URLSearchParams을 이용하면 파라미터 값을 자동으로 쉽게 인코딩할 수 있어요.
// const query = new URLSearchParams(params).toString();
import axios from "axios";

// try {
// const response = await fetch(`${BASE_URL}/products?${query}`);
// if (!response.ok) {
// throw new Error(`HTTP error: ${response.status}`);
// }
// const body = await response.json();
// return body;
// } catch (error) {
// console.error("Failed to fetch products:", error);
// throw error;
// }
// }
const BASE_URL = process.env.REACT_APP_BASE_URL;

export async function getProducts({ page, pageSize, orderBy, keyword }) {
const queryParams = new URLSearchParams({
page,
pageSize,
orderBy,
});
try {
const response = await axios.get(`${BASE_URL}/products`, {
params: { page, pageSize, orderBy, keyword },
});
Comment on lines +7 to +10
Copy link
Collaborator

Choose a reason for hiding this comment

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

axios를 적용해보셨군요 ! 👍

ㅠㅠㅠ 피드백 수용을 넘 잘하세요 👍 좋은 경험이 될거라 생각해요.

다만, 하나 더 제안드리고 싶은건 instance를 생성하고 적용해볼 수 있어요:

어떻게 세팅하면 될까? 🤔

instance를 만들어서 export를 하고 사용해보는 것 정도로 시도해보면 좋을 것 같아요. axios-instance 파일을 만들어서 instance를 생성하고 export한 후 사용해보는건 어떨까요?
다음과 같이 만들어볼 수 있어요:

const baseURL = process.env.NEXT_PUBLIC_LINKBRARY_BaseURL;

const instance = axios.create({
  baseURL: baseURL,
  headers: {
    'Content-Type': 'application/json',
  },
});

export default instance

axios instance

인가에 필요한 accessTokenlocalStorage가 있다면 axios의 인터셉터를 활용할 수 있습니다 !

인터셉터는 혼자 해결해보시는 것을 권장드립니다. 혹시 모르시겠으면 다음 위클리 미션에 질문해주세요. 😊

사용 방법 🚀

사용 방법은 정말 간단해요. 다음과 같이 사용할 수 있습니다:

instance.get(`/user/${userId}`)

딱 보니. 마이그레이션도 정말 쉽게 할 수 있겠죠? 😊

axios API


if (keyword) {
queryParams.append("keyword", keyword);
return response.data;
} catch (error) {
throw new Error(`HTTP error: ${error.response?.status || error.message}`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

(제안) error.message를 먼저 반환시키는건 어떨까요?

Suggested change
throw new Error(`HTTP error: ${error.response?.status || error.message}`);
throw new Error(`HTTP error: ${error.response?.message || error.status}`);

message에 서버로부터의 에러가 포함되어있을 수 있을 것 같아요.
만약 서버 에러 메시지가 포함되어있다면 유저에게 노출할 수도 있겠구요 !
상태만 반환하게 된다면 어떤 에러인지 유저, 그리고 컴포넌트에서도 파악하기 어려울 수 있겠죠?:

example: 400 상태만 가지고는 이메일 형식 불일치인지, 빈 값인지 알 수 없을거예요.

}

const response = await fetch(
`https://panda-market-api.vercel.app/products?${queryParams.toString()}`
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}

return response.json();
}
5 changes: 5 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.
6 changes: 6 additions & 0 deletions src/assets/images/icons/upload_de.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/assets/images/icons/upload_sm.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 68 additions & 0 deletions src/components/ImageUploader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// ImageUploader.jsx
import * as S from "./ImageUploader.styles";
import upload_de from "../assets/images/icons/upload_de.svg";
import ic_X from "../assets/images/icons/ic_X.svg";
import { useState } from "react";

export default function ImageUploader() {
const [previewImage, setPreviewimage] = useState(null);
const [showLimitText, setShowLimitText] = useState(false);

const handleImageChange = (e) => {
const file = e.target.files[0];
if (file) {
const imageUrl = URL.createObjectURL(file);
setPreviewimage(imageUrl);
setShowLimitText(false); // 새로 업로드 성공했으니 문구 지우기
}
};

const handleRemoveImage = () => {
setPreviewimage(null);
setShowLimitText(false); // 이미지 삭제했으니 문구 지우기
};

const handleUploadClick = (e) => {
if (previewImage) {
e.preventDefault();
setShowLimitText(true); // 문구 보여주기
}
};

const isImageUploaded = !!previewImage;
Comment on lines +7 to +32
Copy link
Collaborator

Choose a reason for hiding this comment

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

굿굿 ! 컴포넌트가 참 깔끔한데요? 💯


return (
<S.UploadContainer>
{/* 업로드 버튼 */}
<div>
<S.UploadLabel htmlFor="imageUpload">
<S.ImageUploadIcon src={upload_de} alt="이미지 업로드 아이콘" />
</S.UploadLabel>
<S.HiddenInput
id="imageUpload"
type="file"
accept="image/*"
onChange={handleImageChange}
onClick={handleUploadClick}
/>
{showLimitText && (
<S.LimitText>*이미지 등록은 최대 1장까지 가능합니다.</S.LimitText>
)}
</div>

{/* 이미지 미리보기 */}
{isImageUploaded && (
<div>
<S.PreviewWrapper>
<S.ImageContainer>
<S.PreviewImage src={previewImage} alt="상품 이미지 미리보기" />
<S.RemoveButton onClick={handleRemoveImage}>
<S.CloseIcon src={ic_X} alt="삭제" />
</S.RemoveButton>
</S.ImageContainer>
</S.PreviewWrapper>
</div>
)}
</S.UploadContainer>
);
}
56 changes: 56 additions & 0 deletions src/components/ImageUploader.styles.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import styled from "styled-components";
import theme from "../styles/theme";

export const UploadContainer = styled.div`
display: flex;
gap: 24px;
`;

export const UploadLabel = styled.label`
width: 100%;
height: 100%;
`;

export const ImageUploadIcon = styled.img``;

export const HiddenInput = styled.input`
display: none;
`;

export const PreviewWrapper = styled.div`
position: relative;
display: inline-block;
`;

export const ImageContainer = styled.div`
position: relative;
width: 282px;
height: 282px;
border-radius: 12px;
overflow: hidden;
`;

export const PreviewImage = styled.img`
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
`;

export const RemoveButton = styled.button`
position: absolute;
top: 12px;
right: 12px;
cursor: pointer;
`;

export const CloseIcon = styled.img`
width: 24px;
height: 24px;
`;

export const LimitText = styled.p`
color: ${theme.colors.Error100};
font: ${theme.fonts.H5Regular};
margin-top: 8px;
`;
50 changes: 50 additions & 0 deletions src/components/TagInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// TagInput.jsx
import * as S from "./TagInput.styles";
import ic_x from "../assets/images/icons/ic_X.svg";
import { useState } from "react";

export default function TagInput({ onChange }) {
const [tags, setTags] = useState([]);
const [inputValue, setInputValue] = useState("");

const handleKeyDown = (e) => {
if (e.key === "Enter" && inputValue.trim().length > 1) {
e.preventDefault();

if (!tags.includes(inputValue.trim())) {
const updatedTags = [...tags, inputValue.trim()];
setTags(updatedTags);
Comment on lines +15 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

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

굿굿 ! 배열을 새로 만들어서 상태를 변경하셨군요 ! 👍

배열의 경우 요소를 추가/삭제 하는 것이 아닌 새로운 주소의 배열로 변경을 해줘야하지요. 훌륭합니다 😊

onChange(updatedTags);
}
setTimeout(() => setInputValue(""), 0); // 입력 후 초기화 + 엔터 눌렀을 때 새로고침 방지 비동기처리
Copy link
Collaborator

Choose a reason for hiding this comment

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

setTimeout를 없애도 되지 않을까요? 🤔

Suggested change
setTimeout(() => setInputValue(""), 0); // 입력 후 초기화 + 엔터 눌렀을 때 새로고침 방지 비동기처리
setInputValue("")

}
};

const handleDeleteTag = (tagToDelete) => {
const updatedTags = tags.filter((tag) => tag !== tagToDelete);
setTags(updatedTags);
onChange(updatedTags);
};

return (
<S.TagWrapper>
<S.StyledInput
type="text"
placeholder="태그를 입력해주세요"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
/>
<S.TagList>
{tags.map((tag) => (
<S.TagItem key={tag}>
{tag}
<S.DeleteButton onClick={() => handleDeleteTag(tag)}>
<img src={ic_x} alt="삭제" />
</S.DeleteButton>
</S.TagItem>
))}
</S.TagList>
</S.TagWrapper>
);
}
42 changes: 42 additions & 0 deletions src/components/TagInput.styles.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import styled from "styled-components";
import theme from "../styles/theme";

export const TagWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 14px;
`;

export const StyledInput = styled.input`
width: 100%;
padding: 16px 24px;
gap: 10px;
border: none;
border-radius: 12px;
background-color: ${theme.colors.Gray100};
font: ${theme.fonts.H5Regular};
`;

export const TagList = styled.div`
display: flex;
flex-wrap: wrap;
height: 36px;
gap: 12px;
`;

export const TagItem = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 36px;
padding: 6px 12px 6px 16px;
border-radius: 26px;
background-color: ${theme.colors.Gray100};
color: ${theme.fonts.H5Regular};
gap: 10px;
`;

export const DeleteButton = styled.button`
width: 24px;
height: 24px;
`;
3 changes: 1 addition & 2 deletions src/components/common/Card.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import ic_heart from "../../assets/images/icons/ic_heart.svg";
import placeholder from "../../assets/images/placeholder.jpg";

export default function Card({ item, size }) {
const imageURL =
item.images && item.images.length > 0 ? item.images[0] : placeholder;
const imageURL = item?.images?.[0] ?? placeholder;

return (
<CardWrapper size={size}>
Expand Down
Loading
Loading