Skip to content

Conversation

@junye0l
Copy link
Collaborator

@junye0l junye0l commented Sep 21, 2025

배포

배포

요구사항

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

기본

중고마켓

  • 중고마켓 페이지 주소는 “/items” 입니다.
  • 페이지 주소가 “/items” 일때 상단네비게이션바의 '중고마켓' 버튼의 색상은 “3692FF”입니다.
  • 상단 네비게이션 바는 이전 미션에서 구현한 랜딩 페이지와 동일한 스타일로 만들어 주세요.
  • 상품 데이터 정보는 https://panda-market-api.vercel.app/docs/#/ 에 명세된 GET 메소드 “/products” 를 사용해주세요.
  • '상품 등록하기' 버튼을 누르면 “/additem” 로 이동합니다. ( 빈 페이지 )
  • 전체 상품에서 드롭 다운으로 “최신 순” 또는 “좋아요 순”을 선택해서 정렬을 할 수 있습니다.

중고마켓 반응형

베스트 상품

  • Desktop : 4개 보이기
  • Tablet : 2개 보이기
  • Mobile : 1개 보이기

전체 상품

  • Desktop : 10개 보이기
  • Tablet : 6개 보이기
  • Mobile : 4개 보이기

심화

  • 페이지 네이션 기능을 구현합니다.

주요 변경사항

  • 기본, 심화 요구사항 구현 완료

스크린샷

Desktop Tablet Mobile

멘토에게

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@junye0l junye0l requested a review from kiJu2 September 21, 2025 03:48
@junye0l junye0l self-assigned this Sep 21, 2025
@junye0l junye0l added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Sep 21, 2025
@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 22, 2025

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

벡터 이미지는 svg로 저장하시는걸 추천드려요 !

이하 MDN
SVG(Scalable Vector Graphics)는 2차원 벡터 그래픽을 서술하는 XML 기반의 마크업 언어입니다. SVG는 텍스트 기반의 열린 웹 표준 중 하나로, 모든 사이즈에서 깔끔하게 렌더링 되는 이미지를 서술하며 CSS, DOM, JavaScript, SMIL (en-US) 등 다른 웹 표준과도 잘 동작하도록 설계됐습니다. SVG는 달리 말하자면 HTML과 텍스트의 관계를 그래픽에 적용한 것입니다

image

이미지 파일은 최소 단위가 픽셀로 되어있으며 확대했을 때 이미지가 깨질 수 있어요 !
피그마에서 export하실 때에 svgexport할 수 있습니다 😊

Rester vs Vector

Comment on lines +5 to +8
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

굿굿 ~ axios를 사용하셨군요 !

훌륭합니다 ! 안그래도 추천드리려 했는데 ㅎㅎㅎ
환경 변수도 잘 적용하셨네요 👍👍👍

return response;
},
(error) => {
console.error("API 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.

오호. 디버깅용으로 로깅하셨나보군요?

라이브러리를 이것저것 활용해보려는 시도가 너무 좋군요 !
인터셉터를 활용하여 로깅도 해보시고 👍👍

// api/products/getProducts.js
import apiClient from "../axiosInstance";

export const getProducts = async (params = {}) => {
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
export const getProducts = async (params = {}) => {
export const getProducts = async ({
page = 1,
pageSize = 10,
orderBy = "recent",
keyword = "",
} = {}) => {

(더 나아가서) 주석으로 명시하는 방법도 있습니다 !

다음과 같이 작성해볼 수 있어요:

/**
 * 상품 목록을 가져오는 API
 *
 * @param {Object} options - 요청 옵션
 * @param {number} [options.page=1] - 페이지 번호
 * @param {number} [options.pageSize=10] - 한 페이지에 보여줄 아이템 수
 * @param {"recent"|"favorite"} [options.orderBy="recent"] - 정렬 기준
 * @param {string} [options.keyword] - 검색 키워드
 * @returns {Promise<Object>} - 상품 목록 데이터
 * @throws {Error} API 호출 실패 시 에러 메시지
 *
 * @example
 * // 일반 상품 조회
 * const products = await getProducts({ page: 2, pageSize: 20 });
 *
 * @example
 * // 베스트 상품 조회
 * const bestProducts = await getProducts({ page: 1, pageSize: 4, orderBy: "favorite" });
 */
export const getProducts = async ({
  page = 1,
  pageSize = 10,
  orderBy = "recent",
  keyword,
} = {}) => {

만약 타입스크립트를 사용한다면 타입만 명시해주면 되겠지만, 지금은 자바스크립트이기에 위와 같이 jsdoc을 통해서 주석을 추가해볼 수 있습니다 😉

Comment on lines +7 to +12
params: {
page: params.page || 1,
pageSize: params.pageSize || 10,
orderBy: params.orderBy || "recent",
...(params.keyword && { keyword: params.keyword }),
},
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
params: {
page: params.page || 1,
pageSize: params.pageSize || 10,
orderBy: params.orderBy || "recent",
...(params.keyword && { keyword: params.keyword }),
},
params: {
page,
pageSize,
orderBy,
keyword,
},

Comment on lines +15 to +21
} catch (error) {
throw new Error(
error.response?.data?.message ||
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.

조건문이 복잡한 경우 별칭을 사용해볼 수도 있어요 😉

Suggested change
} catch (error) {
throw new Error(
error.response?.data?.message ||
error.message ||
"상품을 불러오는데 실패했습니다."
);
}
} catch (error) {
const hasMessage = error.response?.data?.message || error.message;
const fallbackMessage = "상품을 불러오는데 실패했습니다.";
throw new Error(hasMessage || fallbackMessage);
}

논리 연산자가 2 개 이상 들어가면 가독성이 저하될 수 있으므로 위와 같이 별칭(hasMessage)을 정의해볼 수 있습니다 😉

Comment on lines +4 to +21
export const getBestProducts = async (limit = 4) => {
try {
const response = await apiClient.get("/products", {
params: {
page: 1,
pageSize: limit,
orderBy: "favorite",
},
});
return response.data;
} catch (error) {
throw new Error(
error.response?.data?.message ||
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.

(이어서) 해당 함수는 이제 필요 없겠군요 !

Suggested change
export const getBestProducts = async (limit = 4) => {
try {
const response = await apiClient.get("/products", {
params: {
page: 1,
pageSize: limit,
orderBy: "favorite",
},
});
return response.data;
} catch (error) {
throw new Error(
error.response?.data?.message ||
error.message ||
"베스트 상품을 불러오는데 실패했습니다."
);
}
};

getProducts에서 orderBy도 고려하므로 다음과 같이 호출하면 현재 함수와 같은 기능을 수행하겠어요 😊:

    const bestProducts = await getProducts({
      page: 1,
      pageSize: 4,
      orderBy: "favorite",
    });

Comment on lines +4 to +26
export const searchProducts = async (keyword, params = {}) => {
try {
if (!keyword || !keyword.trim()) {
throw new Error("검색어를 입력해주세요.");
}

const response = await apiClient.get("/products", {
params: {
page: params.page || 1,
pageSize: params.pageSize || 10,
orderBy: params.orderBy || "recent",
keyword: keyword.trim(),
},
});
return response.data;
} catch (error) {
throw new Error(
error.response?.data?.message ||
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.

해당 함수도 제안해드린 getProducts를 사용해볼 수 있겠군요 😉

Comment on lines +4 to +46
const useResponsiveCount = () => {
const [screenSize, setScreenSize] = useState("desktop");

useEffect(() => {
const updateScreenSize = () => {
const width = window.innerWidth;

if (width < 768) {
setScreenSize("mobile");
} else if (width < 1024) {
setScreenSize("tablet");
} else {
setScreenSize("desktop");
}
};

// 초기 설정
updateScreenSize();

// 윈도우 리사이즈 이벤트 리스너
window.addEventListener("resize", updateScreenSize);

// 클린업
return () => window.removeEventListener("resize", updateScreenSize);
}, []);

// 베스트 상품 개수
const getBestProductCount = () => {
switch (screenSize) {
case "mobile":
return 1; // 모바일: 1개
case "tablet":
return 2; // 태블릿: 2개
case "desktop":
return 4; // 데스크톱: 4개
default:
return 4;
}
};

// 전체 상품 개수 (한 페이지당)
const getAllProductCount = () => {
switch (screenSize) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

해당 커스텀 훅은 현재 꽤나 많은 책임을 지고 있군요.

  • 스크린 크기에 따라서 '전체 상품 개수'에 대한 콜백도 반환해야 되고
  • 클래스 이름도 반환해야 하고
  • '베스트 상품 개수'에 대한 콜백도 반환해야 하네요 !

자 그럼 만약 베스트 상품 개수, 전체 상품 개수와 더불어서 '유저 맞춤 추천 상품'이라는 값이 추가된다면 해당 커스텀 훅을 사용할 수 있을까요?

지금은 사용하기 어려울거예요. 왜냐하면 해당 함수와 엮인 개념들이 많아서 그렇습니다.
조금 경량화 할 수 있을거예요 예를 들어 다음과 같이요 ! :

const useDevice = () => {
  const [device, setDevice] = useState("desktop");

  useEffect(() => {
    const updateDevice = () => {
      const width = window.innerWidth;

      if (width < 768) {
        setDevice("mobile");
      } else if (width < 1024) {
        setDevice("tablet");
      } else {
        setDevice("desktop");
      }
    };

    updateDevice();
    window.addEventListener("resize", updateDevice);
    return () => window.removeEventListener("resize", updateDevice);
  }, []);

  return device; // "mobile" | "tablet" | "desktop"
};

export default useDevice;

Comment on lines +11 to +25

function App() {
// 상태 관리
const [sortBy, setSortBy] = useState("latest");
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalCount, setTotalCount] = useState(0);

// 반응형 훅 사용
const { getAllProductCount, screenSize } = useResponsiveCount();
const pageSize = getAllProductCount();

useEffect(() => {
setCurrentPage(1);
}, [screenSize]);
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
function App() {
// 상태 관리
const [sortBy, setSortBy] = useState("latest");
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalCount, setTotalCount] = useState(0);
// 반응형 훅 사용
const { getAllProductCount, screenSize } = useResponsiveCount();
const pageSize = getAllProductCount();
useEffect(() => {
setCurrentPage(1);
}, [screenSize]);
export const ALL_PRODUCT_COUNT = {
mobile: 4,
tablet: 6,
desktop: 10,
};
function App() {
// 상태 관리
const [sortBy, setSortBy] = useState("latest");
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalCount, setTotalCount] = useState(0);
// 반응형 훅 사용
const device = useDevice();
const pageSize = ALL_PRODUCT_COUNT[device];
useEffect(() => {
setCurrentPage(1);
}, [device]);

자. 이렇게 변경하면 useDevice는 이제 device에 대한 상태만 반환하므로 더욱 다양하게 활용할 수 있을거예요.
처음에 말씀드렸던 "유저 맞춤 추천 상품"등과 같은 개념들이 생겨나도 거뜬하겠죠? 💪

@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 22, 2025

준열님 ~~~ 정말 오랜만이예요 ㅎㅎㅎ
간만에 준열님 코드를 보게되는군요 ! 반가운 마음으로 리뷰하였습니다!
지금처럼 미션 꾸준히해서 계속해서 함께 성장해봅시다 ! 💪

궁금하신거 있으시면 편하게 DM주세요 준열님 !

@kiJu2 kiJu2 merged commit d9a56f7 into codeit-bootcamp-frontend:React-김준열 Sep 22, 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