Skip to content

Conversation

@BaeZzi813
Copy link
Collaborator

요구사항

Github에 PR(Pull Request)을 만들어서 미션을 제출합니다.
피그마 디자인에 맞게 페이지를 만들어 주세요.
React를 사용합니다

기본

  • 상품 등록 페이지 주소는 "/additem" 입니다.
  • 페이지 주소가 "/additem" 일때 상단네비게이션바의 '중고마켓' 버튼의 색상은 "3692FF"입니다.
  • 상품 이미지는 최대 한개 업로드가 가능합니다.
  • 각 input의 placeholder 값을 정확히 입력해주세요.
  • 이미지를 제외하고 input 에 모든 값을 입력하면 '등록' 버튼이 활성화 됩니다.
  • API를 통한 상품 등록은 추후 미션에서 적용합니다.

심화

  • 이미지 안의 X 버튼을 누르면 이미지가 삭제됩니다.
  • 추가된 태그 안의 X 버튼을 누르면 해당 태그는 삭제됩니다.

주요 변경사항

스크린샷

PC Tablet Mobile
PC 화면 Tablet 화면 Mobile 화면

멘토에게

  • 이미지 미리보기 기능에서 서버 송신을 위해서 FileReader() 을 사용하였는데 URL.createObjectURL()을 사용하는 것이 더 적절한지 궁금합니다.
  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@BaeZzi813 BaeZzi813 requested a review from kiJu2 August 9, 2025 08:30
@BaeZzi813 BaeZzi813 added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Aug 9, 2025
@kiJu2 kiJu2 changed the base branch from main to React-양재영 August 11, 2025 04:25
@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 11, 2025

스프리트 미션 하시느라 수고 많으셨어요.
재영님 학습에 도움 되실 수 있게 꼼꼼히 리뷰 하도록 해보겠습니다. 😊

return;
}

const reader = new FileReader();
Copy link
Collaborator

Choose a reason for hiding this comment

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

이미지 미리보기 기능에서 서버 송신을 위해서 FileReader() 을 사용하였는데 URL.createObjectURL()을 사용하는 것이 더 적절한지 궁금합니다.

FileReader

The FileReader interface lets web applications asynchronously read the contents of files (or raw data buffers) stored on the user's computer, using File or Blob objects to specify the file or data to read.
이 FileReader인터페이스를 사용하면 웹 애플리케이션이 사용자 컴퓨터에 저장된 파일(또는 원시 데이터 버퍼)의 내용을 비동기적으로 읽을 수 있으며, File또는 Blob객체를 사용하여 읽을 파일이나 데이터를 지정할 수 있습니다.

FileReader.readAsDataURL()

Starts reading the contents of the specified Blob, once finished, the result attribute contains a data: URL representing the file's data.

지정된 파일의 내용을 읽기 시작하고 Blob, 완료되면 속성에 파일 데이터를 나타내는 URL이 result포함됩니다 .

저장된 파일의 버퍼 혹은 원시 데이터의 내용을 읽을 수 있게 해주는 API군요.
이렇게 사용하여도 URL을 지원되긴 하지만 오버헤드가 있을 수도 있겠어요.

오버헤드: 어떤 처리에 필요한 간접적인 시간이나 자원을 의미합니다. 즉, 주된 작업 외에 추가적으로 발생하는 비용이나 노력을 말합니다.

직접 테스트를 통해서 얼만큼의 시간 비용 절약이 있는지 확인해보면 좋겠으나, 문서만 보기에는 말씀 주신대로 '미리보기'에는 createObjectURL 사용하시는게 더 나을 수 있을 것 같아요. 😉😉

Comment on lines +89 to +104
const handleFileChange = (e) => {
const file = e.target.files[0];
if (!file) return;

if (previewImg) {
SetShowError(true);
inputRef.current.value = "";
return;
}

const reader = new FileReader();
reader.onloadend = () => {
setPreviewImg(reader.result);
};
reader.readAsDataURL(file);
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

(이어서) 변경된다음 다음과 같이 변경될 수 있겠군요 ! 😉

Suggested change
const handleFileChange = (e) => {
const file = e.target.files[0];
if (!file) return;
if (previewImg) {
SetShowError(true);
inputRef.current.value = "";
return;
}
const reader = new FileReader();
reader.onloadend = () => {
setPreviewImg(reader.result);
};
reader.readAsDataURL(file);
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// 예: 타입/용량 검증
if (!file.type.startsWith("image/")) {
setError("이미지 파일만 업로드할 수 있습니다.");
e.target.value = "";
return;
}
setError(null);
fileRef.current = file;
// 이전 URL 정리
if (previewUrl) URL.revokeObjectURL(previewUrl);
// 미리보기 URL 생성
const url = URL.createObjectURL(file);
setPreviewUrl(url);
};

Comment on lines +15 to +36
const ProductAddHeaderButton = styled.button`
font-weight: 600;
font-size: 1.6rem;
color: var(--gray-100);
background-color: ${({ disabled }) =>
disabled ? "var(--gray-400)" : "var(--blue)"};
border-radius: 0.8rem;
width: 7.4rem;
height: 4.2rem;
border: 0.1rem solid var(--gray-400);
cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")};
`;

export default function ProductAddHeader({ isFormValid }) {
return (
<ProductAddHeaderDiv>
<ProductAddHeaderTitle>상품 등록하기</ProductAddHeaderTitle>
<ProductAddHeaderButton type="submit" disabled={!isFormValid}>
등록
</ProductAddHeaderButton>
</ProductAddHeaderDiv>
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

굿굿 ! buttondisabled 속성에 접근한 점 훌륭합니다 !

다만, css도 상태 선택자를 통해서 관리해볼 수 있을 것 같아요 !:

Suggested change
const ProductAddHeaderButton = styled.button`
font-weight: 600;
font-size: 1.6rem;
color: var(--gray-100);
background-color: ${({ disabled }) =>
disabled ? "var(--gray-400)" : "var(--blue)"};
border-radius: 0.8rem;
width: 7.4rem;
height: 4.2rem;
border: 0.1rem solid var(--gray-400);
cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")};
`;
export default function ProductAddHeader({ isFormValid }) {
return (
<ProductAddHeaderDiv>
<ProductAddHeaderTitle>상품 등록하기</ProductAddHeaderTitle>
<ProductAddHeaderButton type="submit" disabled={!isFormValid}>
등록
</ProductAddHeaderButton>
</ProductAddHeaderDiv>
);
const ProductAddHeaderButton = styled.button`
font-weight: 600;
font-size: 1.6rem;
color: var(--gray-100);
border-radius: 0.8rem;
width: 7.4rem;
height: 4.2rem;
border: 0.1rem solid var(--gray-400);
&:enabled {
background-color: var(--blue);
cursor: pointer;
}
&:disabled {
background-color: var(--gray-400);
cursor: not-allowed;
}
`;
export default function ProductAddHeader({ isFormValid }) {
return (
<ProductAddHeaderDiv>
<ProductAddHeaderTitle>상품 등록하기</ProductAddHeaderTitle>
<ProductAddHeaderButton type="submit" disabled={!isFormValid}>
등록
</ProductAddHeaderButton>
</ProductAddHeaderDiv>
);

이렇게 하면 삼항연산자를 사용하지 않고 "특정 상태"에 대한 스타일링을 해볼 수 있습니다. 😉

Comment on lines +9 to +13
try {
const params = new URLSearchParams({ page, pageSize, orderBy, keyword });
const response = await instance.get("/products", { params });
return response.data;
} catch (error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으 ~ axios를 적용해보셨군요 ! 👍👍

훌륭합니다 ! 이 경험을 살려서 기초 팀 프로젝트에도 채택해보실 수 있겠어요.

@@ -0,0 +1,33 @@
import axios from "axios";

const VITE_BASE_URL = import.meta.env.VITE_BASE_URL;
Copy link
Collaborator

Choose a reason for hiding this comment

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

굿굿 ! base url을 환경 변수로 잘 설정하셨군요 👍

Comment on lines +16 to +29
(error) => {
if (error.response) {
const status = error.response.status;

if (status === 401) {
alert("인증이 필요합니다. 로그인 해주세요.");
} else if (status === 500) {
alert("서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
}
} else {
alert("네트워크 오류가 발생했습니다.");
}

return Promise.reject(error);
Copy link
Collaborator

@kiJu2 kiJu2 Aug 11, 2025

Choose a reason for hiding this comment

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

각 상태 코드 별로 예외처리도 해두셨군요. 👍

굿굿. 공통적인 예외처리도 신경쓰시는 모습 보기 좋습니다 ! 👍

다음과 같이 설계해볼 수도 있을 것 같네요:

Suggested change
(error) => {
if (error.response) {
const status = error.response.status;
if (status === 401) {
alert("인증이 필요합니다. 로그인 해주세요.");
} else if (status === 500) {
alert("서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
}
} else {
alert("네트워크 오류가 발생했습니다.");
}
return Promise.reject(error);
export const ERROR_STATUSCODE_MESSAGES = {
401: "인증이 필요합니다. 로그인 해주세요.",
403: "접근 권한이 없습니다.",
404: "요청하신 리소스를 찾을 수 없습니다.",
500: "서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.",
};
// ...
(error) => {
if (error.response) {
const status = error.response.status;
alert(ERROR_STATUSCODE_MESSAGES[status] || "알 수 없는 오류가 발생했습니다.");
} else {
alert("네트워크 오류가 발생했습니다.");
}
return Promise.reject(error);

Comment on lines +3 to +11

export default function SelectDropdown({ onChange, value }) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);

const options = [
{ label: "최신순", value: "recent" },
{ label: "좋아요순", value: "favorite" },
];
Copy link
Collaborator

Choose a reason for hiding this comment

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

options 또한 컴포넌트 외부에 선언해볼 수 있겠네요 😉

현재 options는 컴포넌트 내부의 자원(상태나 props)을 사용하고 있지 않아요.
리렌더링 시 불필요한 재선언이 될 수 있으므로 컴포넌트 바깥에 선언해볼 수 있습니다 😉

Suggested change
export default function SelectDropdown({ onChange, value }) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const options = [
{ label: "최신순", value: "recent" },
{ label: "좋아요순", value: "favorite" },
];
const options = [
{ label: "최신순", value: "recent" },
{ label: "좋아요순", value: "favorite" },
];
export default function SelectDropdown({ onChange, value }) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);

이렇게 되면 컴포넌트 내부에는 컴포넌트 자원만 사용하는 코드들만 남게되어 자연스레 가독성도 좋아질거예요. 👍

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 11, 2025

수고하셨습니다 재영님 !
리액트를 능숙하게 잘 다루시는군요 ! 👍
이제 기초 프로젝트 시작이네요 ㅎㅎㅎ
협업 프로젝트에서도 재영님의 역량이 팀에 멋지게 기여될 수 있을거라 확신합니다 !👍👍

코드잇에서의 재영님의 첫 협업 프로젝트 응원합니다 👍👍

@kiJu2 kiJu2 merged commit 84018cd into codeit-bootcamp-frontend:React-양재영 Aug 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants