Skip to content

Conversation

@sgoldenbird
Copy link
Collaborator

@sgoldenbird sgoldenbird commented May 11, 2025

요구사항

배포 링크: https://codeit-sprint15-mission.netlify.app/

기본

상품 상세

  • 상품 상세 페이지 주소는 "/items/{productId}" 입니다.
  • response 로 받은 아래의 데이터로 화면을 구현합니다.

=> favoriteCount : 하트 개수
=> images : 상품 이미지
=> tags : 상품태그
=> name : 상품 이름
=> description : 상품 설명

  • 목록으로 돌아가기 버튼을 클릭하면 중고마켓 페이지 주소인 "/items" 으로 이동합니다

상품 문의 댓글

  • 문의하기에 내용을 입력하면 등록 버튼의 색상은 "3692FF"로 변합니다.
  • response 로 받은 아래의 데이터로 화면을 구현합니다

=> image : 작성자 이미지
=> nickname : 작성자 닉네임
=> content : 작성자가 남긴 문구
=> description : 상품 설명
=> updatedAt : 문의글 마지막 업데이트 시간

심화

  • 모든 버튼에 자유롭게 Hover효과를 적용하세요.

주요 변경사항

  • dropdown 컴포넌트, useDropdown 훅 생성해서 dropdown버튼들 통합(SortSelect랑, VerticalKebab)
  • dropdown컴포넌트를 textmode와 iconmode로 분리
  • 토스트 메시지를 호출 단으로 이동하여 사용자에게 더 명확한 피드백 제공
  • 상품 등록 성공 시 상세 페이지로 자동 이동 처리
  • fetch 요청을 메서드별로 모듈화하여 재사용성과 가독성 향상
  • 이미지 제거 시 input 초기화로 동일 이미지 재업로드 가능하도록 개선
  • ProductInfo의 버티컬 케밥 버튼에도 수정/삭제 기능 구현
    • '수정하기' 클릭 시 상품 수정 페이지로 이동

스크린샷

image
image
image
image

멘토에게

  • 현재 리액트 라우터의 declaration mode를 사용하고 있습니다. 이번에 Items/:id 경로를 추가하면서 route/index.js에
    <Route path={ROUTES.ITEM_DETAIL()} element={<ProductDetail />} />이렇게 했는데 맞게 한건지 궁금합니다.
  • DropdownBtn 컴포넌트에는 드롭다운 버튼들의 공통 기능과 디자인을 넣고 개별 사용처마다 다른 디자인을 prop으로 넘기는 과정에서 className관련 prop을 너무 많이 쓰게 되서 혹시 다른 방법이 있는지 궁금합니다. 예를 들어 지금 DropdownBtn은 사용처마다 다른 css속성 부분을 buttonClassName과 optionListClassName으로 넘기는게 별로인 것 같습니다.
    (FavoriteBtn 컴포넌트도 마찬가지로 클래스네임 두개로 받고 있어요) 개선 할 수 있는 방법이 있을까요?
  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@sgoldenbird sgoldenbird changed the base branch from main to React-송시은 May 11, 2025 14:54
@sgoldenbird sgoldenbird requested a review from GANGYIKIM May 11, 2025 22:00
@sgoldenbird sgoldenbird self-assigned this May 11, 2025
@sgoldenbird sgoldenbird added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label May 11, 2025
@sgoldenbird sgoldenbird force-pushed the React-송시은-sprint7 branch 2 times, most recently from 26ce3d0 to 3fa953f Compare May 11, 2025 22:19
@sgoldenbird sgoldenbird force-pushed the React-송시은-sprint7 branch from 3fa953f to f8d925a Compare May 11, 2025 22:28
@sgoldenbird sgoldenbird force-pushed the React-송시은-sprint7 branch from 95e6b5e to 4585d11 Compare May 12, 2025 18:03
Copy link
Collaborator

@GANGYIKIM GANGYIKIM left a comment

Choose a reason for hiding this comment

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

시은님 7번째 미션 작업 수고하셨습니다~
요구사항외에도 코멘트 관련 로직들을 구현하려고 하신 것 같아요.
다만 해당 기능들은 로그인 이후 구현이 가능하니 참고해주세요.
저도 이를 감안하고 코드리뷰 진행했습니다~

다음 미션도 화이팅이에요!


DropdownBtn 컴포넌트에는 드롭다운 버튼들의 공통 기능과 디자인을 넣고 개별 사용처마다 다른 디자인을 prop으로 넘기는 과정에서 className관련 prop을 너무 많이 쓰게 되서 혹시 다른 방법이 있는지 궁금합니다. 예를 들어 지금 DropdownBtn은 사용처마다 다른 css속성 부분을 buttonClassName과 optionListClassName으로 넘기는게 별로인 것 같습니다.
(FavoriteBtn 컴포넌트도 마찬가지로 클래스네임 두개로 받고 있어요) 개선 할 수 있는 방법이 있을까요?

공통 컴포넌트란 재사용되는 컴포넌트를 의미하는데, 이렇게 클래스를 여러개 받아야 하는 필요가 있는지 잘 모르겠습니다. 실제로 이렇게 할 필요가 있는지, 아니면 혹시몰라 이렇게 구현하시려고 하는지를 고민해보세요.
만약 디자인 요구사항이 복잡해서 클래스를 여러개 받아야한다면 지금과 같은 구조를 유지하시거나 children으로 받으시거나 합성 컴포넌트 방식을 고려해보실 수 있겠습니다.

const ButtonWrap = () => { ... }
const DropdownIcon = () => { ... }

const CompA = ({className="", children}) => {
  return (
    <ButtonWrap className={className}>
      <DropdownIcon/>
      {children}
    </ButtonWrap>
  )
}

const Page = () => {
  ...
  return (
    <CompA className='...'>
      <div className="...">hellow</div>
    </CompA>
  )
}

https://fe-developers.kakaoent.com/2022/220731-composition-component/

const date = new Date(isoDate);
const diff = (now - date) / 1000;

const rtf = new Intl.RelativeTimeFormat('ko', { numeric: 'auto' });
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 칭찬
적절한 내장 객체 사용 좋아요~

ADD_ITEM: '/additem',
ITEM_DETAIL: (id = ':productId') => `/items/${id}`,
BOARD: '/board',
EDIT_ITEM: (id = ':productId') => `/items/${id}/edit`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
id를 넘겨서 조합하는 곳이 없어서 왜 이렇게 작성하셨는지 이해가 안가요!
이렇게 코딩하신 명확한 이유가 없다면 /items/:productId/edit로 변경하시는 것을 추천합니다.
만약 구조화를 하고 싶으신 거였다면 지금 사용하시는 Declative 모드보다 Data 모드 방식이 더 적절할 것 같아요.

import { createContext, useContext } from 'react';

export const UserContext = createContext(null);
export const useUser = () => useContext(UserContext);
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
해당 context를 사용하는 훅을 정의하셔서 사용성을 높으신 것 좋습니다.
다만 아래처럼 hook 내부에서 해당 context에 접근하지 않을때의 에러도 추가해주시면 더 좋을 것 같습니다!

Suggested change
export const useUser = () => useContext(UserContext);
export const useUserContext = () => {
const _context = useContext(UserContext);
if (!_context) throw new Error(/** error msg */);
return _context;
}

Comment on lines +1 to +23
export const COMMENT_ERROR_MESSAGES = {
// 공통
UNAUTHORIZED: '로그인이 필요합니다.',
SERVER_ERROR: '서버에 문제가 발생했어요. 잠시 후 다시 시도해주세요.',

// GET
FORBIDDEN_FETCH: '이 댓글을 볼 권한이 없어요.',
NOT_FOUND_FETCH: '등록된 문의가 없습니다.',
FETCH_FAILED: '댓글을 불러오는 데 실패했어요.',

// POST
FORBIDDEN_POST: '댓글을 등록할 권한이 없어요.',
POST_FAILED: '댓글을 등록하는 데 실패했어요.',

// PATCH
FORBIDDEN_PATCH: '이 댓글을 수정할 권한이 없어요.',
NOT_FOUND_PATCH: '수정할 댓글을 찾을 수 없어요.',
PATCH_FAILED: '댓글을 수정하는 데 실패했어요.',

// DELETE
FORBIDDEN_DELETE: '이 댓글을 삭제할 권한이 없어요.',
NOT_FOUND_DELETE: '삭제할 댓글을 찾을 수 없어요.',
DELETE_FAILED: '댓글을 삭제하는 데 실패했어요.',
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
아래처럼 객체로 분리해서 의도를 명확하게 하실 수도 있습니다.
다만 메소드별로 묶는 것보다 해당 에러 메시지를 기준으로 명명하시는 게 좋을 것 같아요.
FORBIDDEN_AUTH, FORBIDDEN_EDIT 같은 식이면 더 명확할 것 같습니다.

const COMMON = {
  UNAUTHORIZED: '로그인이 필요합니다.',
  SERVER_ERROR: '서버에 문제가 발생했어요. 잠시 후 다시 시도해주세요.',
}

const FETCH = {
  FORBIDDEN_FETCH: '이 댓글을 볼 권한이 없어요.',
  NOT_FOUND_FETCH: '등록된 문의가 없습니다.',
  FETCH_FAILED: '댓글을 불러오는 데 실패했어요.',
}

export const COMMENT_ERROR_MESSAGES = {
  ...COMMON,
  ...FETCH,
  ...
}

Comment on lines +5 to +9
const VerticalKebabDrop = ({ onSelect }) => {
const options = [
{ label: '수정하기', value: 'edit' },
{ label: '삭제하기', value: 'delete' },
];
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
common 폴더에 있는 만큼, options를 외부에서 받아 다른 메뉴일 때도 사용할 수 있게 설계하시면 좋겠어요!

Suggested change
const VerticalKebabDrop = ({ onSelect }) => {
const options = [
{ label: '수정하기', value: 'edit' },
{ label: '삭제하기', value: 'delete' },
];
const VerticalKebabDrop = ({ options, onSelect }) => {

import buttonStyles from '@/styles/helpers/buttonHelpers.module.scss';
import styles from './CommentInput.module.scss';

const CommentInput = ({ productId, refreshAfterSubmit }) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
CommentInput 컴포넌트 내부에서 refreshAfterSubmit라는 이름으로 함수를 받을 필요가 없어보여요.
이러한 이름은 함수명으로는 적절하지만 해당 컴포넌트 내부에서는 그냥 onSubmit 정도로 추상화하는게 더 적절해보입니다~

Comment on lines +13 to +14
const handleSubmit = async () => {
if (!content.trim()) return;
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 handleSubmit = async () => {
if (!content.trim()) return;
const handleSubmit = async () => {

<div className={styles.edit}>
<button
type="button"
className={buttonStyles.primary}
Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
디자인과 다른 버튼으로 보이네요! primary는 주로 확인, 수정과 같은 button으로 말하자면 submit 버튼일때 적용이 되는 친구입니다~
확인해보시고 디자인대로 수정해주세요!

Copy link
Collaborator

Choose a reason for hiding this comment

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

❗️ 수정요청
지금은 고정 개수 10개로 코멘트를 불러오고 있어 모든 코멘트가 보이고 있지 않아요.
아마 더보기 버튼을 눌렀을때 추가로 불러오도록 작업하려고 하시는 것 같아요.
그것도 좋고 디자인대로 하시려면 무한스크롤 방식으로 구현해주시면 될 것 같습니다~

Comment on lines +17 to +38
const textDropdown = () => (
<button
type="button"
className={styles.textMode}
onClick={toggle}
aria-label={label}
>
{label && <span className={styles.label}>{label}</span>}
<span className={styles.arrow} />
</button>
);

const iconDropdown = () => (
<button
type="button"
className={`${styles.iconMode} ${buttonClassName}`}
onClick={toggle}
aria-label="Open Dropdown"
>
<img src={iconSrc} alt={iconAlt} />
</button>
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

💊 제안
textDropdown과 iconDropdown 를 각각 정의해 조건부 렌더링을 하도록 작성하셨네요.
다만 두 컴포넌트에서 공통되는 부분이 많아 보입니다. 중복을 줄이시면 가독성 측면에서 더 좋을 것 같아요.

const DropdownBtn = ((props) => {
  ...
  return (
    <div className={wrapperClass} ref={dropdownRef}>
      <button
        type="button"
        className={`${styles[mode]} ${className}`}
        onClick={onClick}
        aria-label={mode === 'textMode' ? label : 'Open Dropdown'}
      >
        {mode === 'textMode' ? (
          <>
            {label && <span className={styles.label}>{label}</span>}
            <span className={styles.arrow} />
          </>
        ) : (
          <img src={iconSrc} alt={iconAlt} />
        )}
      </button>
      {isOpen && <OptionList>}
    </div>
  )
}

@GANGYIKIM GANGYIKIM merged commit d309606 into codeit-bootcamp-frontend:React-송시은 May 16, 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